Looking to build a modern, scalable blog with a clean editorial experience? Python Wagtail CMS offers a powerful, developer‑friendly framework that blends the flexibility of Django with a sleek, drag‑and‑drop admin interface. In this guide we’ll walk you through creating a full‑featured blog project from scratch, covering everything from initial setup and content models to SEO optimization and deployment. By the end, you’ll have a production‑ready Wagtail blog that’s easy to maintain and primed for search engine visibility.
Why Choose Wagtail for a Python Blog Project?
Wagtail has quickly become a favorite among developers and content editors alike. Here are the key reasons it stands out for a blog project:
- Built on Django – Leverages the robustness, security, and extensive ecosystem of the world’s most popular Python web framework.
- Intuitive admin UI – Content creators can compose, reorder, and schedule posts without touching code.
- Streamlined routing – Powerful page hierarchy and URL handling out of the box.
- Rich content blocks – Reusable StreamField blocks let you embed images, videos, and custom widgets.
- SEO‑ready – Built‑in meta tags, sitemap generation, and easy integration with third‑party SEO tools.
Project Prerequisites
Before diving into code, make sure you have the following installed on your development machine:
- Python 3.10 or newer
- Virtualenv or
venvfor isolated environments - Git (optional but recommended)
- PostgreSQL (recommended for production; SQLite works for quick prototyping)
Step 1: Setting Up the Wagtail Project
Start by creating a fresh virtual environment and installing Wagtail via pip:
python -m venv env
source env/bin/activate # On Windows use `env\Scripts\activate`
pip install wagtail
wagtail start myblog
cd myblog
pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
Visit http://127.0.0.1:8000/ in your browser and log in with the superuser credentials you just created. You’ll see the default Wagtail admin dashboard – the perfect launchpad for your blog.
Step 2: Designing the Blog Content Model
In Wagtail, each piece of content is a Page subclass. For a blog, we’ll create two page types:
- BlogIndexPage – Serves as the container for all blog posts and handles pagination.
- BlogPostPage – Represents an individual article with title, author, publish date, body, and SEO fields.
Open home/models.py (or create a new app called blog) and add the following code:
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel, MultiFieldPanel
from wagtail.core.models import Page
from wagtail.core.fields import StreamField
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.core import blocks
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
class BlogIndexPage(Page):
intro = models.TextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('intro')
]
subpage_types = ['blog.BlogPostPage']
def get_context(self, request):
# Add blog posts to the template context
context = super().get_context(request)
posts = BlogPostPage.objects.live().descendant_of(self).order_by('-first_published_at')
context['posts'] = posts
return context
class BlogPostPage(Page):
date = models.DateField("Post date")
author = models.CharField(max_length=255)
body = StreamField([
('paragraph', blocks.RichTextBlock()),
('image', blocks.ImageChooserBlock()),
('quote', blocks.BlockQuoteBlock()),
], null=True, blank=True)
# SEO fields
meta_description = models.CharField(max_length=160, blank=True)
content_panels = Page.content_panels + [
MultiFieldPanel([
FieldPanel('date'),
FieldPanel('author'),
], heading="Post Details"),
StreamFieldPanel('body'),
FieldPanel('meta_description', classname="full"),
]
parent_page_types = ['blog.BlogIndexPage']
subpage_types = []
Run migrations to apply the new models:
python manage.py makemigrations blog
python manage.py migrate
Step 3: Creating Templates for Index and Detail Pages
Wagtail looks for templates named after the page class. Create two files inside blog/templates/blog/:
blog_index_page.htmlblog_post_page.html
blog_index_page.html – displays a list of posts with pagination:
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
{{ page.title }}
{% if page.intro %}
{{ page.intro }}
{% endif %}
{% for post in posts %}
-
{{ post.title }}
{% if post.search_description %}
{{ post.search_description }}
{% endif %}
{% empty %}
- No posts yet.
{% endfor %}
{% endblock %}
blog_post_page.html – renders the full article with SEO meta tags:
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block head %}
{{ block.super }}
{% if page.meta_description %}
{% endif %}
{% endblock %}
{% block content %}
{{ page.title }}
{% for block in page.body %}
{% include_block block %}
{% endfor %}
{% endblock %}
Step 4: Adding Rich Features with StreamField
StreamField makes it simple to let editors embed complex content without developers writing extra code each time. Below are a few popular block types you might want to add:
- Image Gallery – a collection of images displayed as a carousel.
- Embedded Video – YouTube or Vimeo embeds with responsive sizing.
- Call‑to‑Action (CTA) – highlighted sections that drive newsletter sign‑ups or product links.
Extend the body definition in BlogPostPage like so:
body = StreamField([
('paragraph', blocks.RichTextBlock()),
('image', blocks.ImageChooserBlock()),
('quote', blocks.BlockQuoteBlock()),
('gallery', blocks.ListBlock(blocks.ImageChooserBlock())),
('video', blocks.StructBlock([
('url', blocks.URLBlock()),
('caption', blocks.CharBlock(required=False)),
])),
('cta', blocks.StructBlock([
('text', blocks.CharBlock()),
('button_text', blocks.CharBlock()),
('button_link', blocks.URLBlock()),
])),
], null=True, blank=True)
Remember to run makemigrations again after updating the model.
Step 5: SEO Best Practices for a Wagtail Blog
Search engine visibility is crucial for any blog. Wagtail provides built‑in tools, but you should also implement a few extra steps:
1. Enable the Sitemap Generator
Add 'wagtail.contrib.sitemaps' to INSTALLED_APPS and include the sitemap URL in urls.py:
from wagtail.contrib.sitemaps.views import sitemap
urlpatterns = [
# … other patterns …
path('sitemap.xml', sitemap),
]
2. Configure Meta Tags and Open Graph
Use the meta_description field you added and create a reusable snippet for Open Graph data. Example snippet model:
class SocialMetaSnippet(models.Model):
page = ParentalKey('wagtailcore.Page', related_name='social_meta')
og_title = models.CharField(max_length=60, blank=True)
og_description = models.CharField(max_length=155, blank=True)
og_image = models.ForeignKey(
'wagtailimages.Image',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
panels = [
FieldPanel('og_title'),
FieldPanel('og_description'),
ImageChooserPanel('og_image'),
]
Then render these tags in the head block of your base template.
3. Optimize Images
Leverage Wagtail’s built‑in image rendition system to serve appropriately sized images:
Leave a Reply