Most CMS-driven SEO conversations get stuck in the same loop: marketing asks "can we add this tag?" — engineering says "we'll need a plugin" — three months later there's a dependency in production that nobody owns. Wagtail's model breaks that loop. Because page schemas are defined in Python, you express your SEO requirements as part of the content model itself. They can't drift, they're version-controlled, and they enforce themselves on every editor — no plugin required.
This guide walks through everything we configure on a serious Wagtail SEO build, in the order we'd introduce it on a real project. The audience is mixed: SEO specialists and marketers should walk away knowing what to ask for; developers should walk away knowing how to implement it.
011. Why Wagtail wins on SEO
Three structural reasons Wagtail outperforms most CMS choices on SEO before you've written a single line of custom code:
- Editor freedom inside engineered constraints. Editors can't accidentally publish a page with no
<title>, a duplicate H1, or a missing meta description — because the page model says they can't. Brand voice is preserved by code, not by review meetings. - Server-rendered HTML by default. Search engines see fully-rendered HTML on first request, with structured data inline. No JavaScript hydration races, no SSR/CSR mismatch, no "Google waited 6 weeks to re-render and decide your page exists."
- Direct database access to content. Building bespoke sitemaps, programmatic redirects, or feeding an indexable JSON feed to an external search engine is a 20-line view. There is no integration to license, no API quota to plan against.
SEO on Wagtail isn't a checklist of plugins to install — it's a set of constraints to encode into your page models. Get that right at the schema stage and every subsequent SEO improvement is a small, predictable PR rather than a fragile workaround.
022. Built-in Wagtail SEO features
Wagtail ships with five SEO primitives most teams under-use. Before reaching for any third-party package, walk through what's already in the box.
Page promote-tab fields
Every Page subclass inherits slug, seo_title, search_description, and a show_in_menus toggle, all exposed in the editor's Promote tab. The seo_title field overrides the page's content title in the <title> tag when set — letting marketers tune SERP titles independently of editorial headlines.
URL routing & slug control
Wagtail's URL routing is hierarchical and editor-controlled. Moving a page in the admin tree changes its URL — and the built-in RedirectMiddleware automatically creates a 301 redirect from the old URL so inbound links don't break. You can also create manual Redirects from the admin under Settings → Redirects, which is invaluable for handling SEO continuity during a migration.
Search promotions
The wagtail.contrib.search_promotions module lets editors curate which pages appear for which on-site search queries — the Wagtail equivalent of "best bets" in enterprise search. Useful for promoting cornerstone content and capturing on-site search intent that signals future SEO targets.
The accessibility checker
Wagtail's admin includes an Axe-core accessibility checker that audits each page before publication. The link to SEO is direct: Google's Core Web Vitals include accessibility-adjacent signals (focus visibility, labelled controls), and good document structure correlates strongly with featured-snippet eligibility. Run the checker as a gate, not an option.
Live preview & draft revisions
Editors can preview SEO-relevant changes (titles, descriptions, slugs, OG images) before publication. Draft revisions mean a marketing experiment that misfires can be rolled back in one click without losing the editorial history.
033. Metadata models & Django SEO packages
The built-in fields cover the basics. For a serious SEO build you'll want explicit fields for Open Graph image, Twitter card type, canonical URL override, robots meta, and structured-data inputs. Two paths to get there: write your own abstract model (the approach we prefer for full control) or use a community package.
The "write it once" abstract model
from django.db import models from wagtail.models import Page from wagtail.admin.panels import MultiFieldPanel, FieldPanel from wagtail.images import get_image_model_string class SeoMixin(models.Model): """Drop on any Page subclass to get a consistent SEO field set.""" canonical_url = models.URLField( blank=True, help_text="Override the canonical URL. Leave blank for the default.", ) og_image = models.ForeignKey( get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="1200×630 — used for Facebook, LinkedIn, Slack previews.", ) twitter_card = models.CharField( max_length=20, choices=[("summary", "Summary"), ("summary_large_image", "Summary with large image")], default="summary_large_image", ) robots_index = models.BooleanField(default=True, help_text="Allow indexing") robots_follow = models.BooleanField(default=True, help_text="Follow links") seo_panels = [ MultiFieldPanel([ FieldPanel("canonical_url"), FieldPanel("og_image"), FieldPanel("twitter_card"), FieldPanel("robots_index"), FieldPanel("robots_follow"), ], heading="Search & social"), ] class Meta: abstract = True class ArticlePage(SeoMixin, Page): body = ... promote_panels = Page.promote_panels + SeoMixin.seo_panels
Then in your base template, render the metadata defensively — if a page hasn't set a value, fall back to a sensible default:
<title>{{ page.seo_title|default:page.title }} | {{ settings.core.SiteSettings.site_name }}</title>
<meta name="description" content="{{ page.search_description|default:page.specific.body|striptags|truncatewords:30 }}">
<link rel="canonical" href="{{ page.canonical_url|default:page.get_full_url }}">
<meta name="robots" content="{% if page.robots_index %}index{% else %}noindex{% endif %},{% if page.robots_follow %}follow{% else %}nofollow{% endif %}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{ page.get_full_url }}">
<meta property="og:title" content="{{ page.seo_title|default:page.title }}">
<meta property="og:description" content="{{ page.search_description }}">
{% if page.og_image %}
{% image page.og_image fill-1200x630 as ogimg %}
<meta property="og:image" content="{{ ogimg.full_url }}">
</meta>
{% endif %}
<meta name="twitter:card" content="{{ page.twitter_card }}">
Useful Django SEO packages
wagtail-seo(which we co-maintain) — drop-in SEO mixin, Open Graph helpers, JSON-LD Article and Organization schema, sensible defaults that match what Google actually parses. The pragmatic starting point.wagtail-metadata— older, lighter-weight alternative. Worth knowing about; pick one or the other, not both.django-meta— Django-level (not Wagtail-specific) meta tag generation. Useful if your site mixes Wagtail with Django views.wagtail-localize— multi-language with built-inhreflanggeneration. Mandatory if your site serves more than one locale.django-htmlmin+django-compressor— minification and bundling. Small wins but real on Core Web Vitals.
044. Sitemap generation
Wagtail ships a sitemap generator that's smarter than Django's default. It walks the live page tree, skips pages flagged not to appear in sitemaps, and respects the same multi-site routing as your URL handlers. Wire it up in your urls.py and you have an XML sitemap that updates the instant editors publish.
from django.urls import path from wagtail.contrib.sitemaps.views import sitemap urlpatterns = [ path("sitemap.xml", sitemap), ... ]
What it gives you for free: <lastmod> from each page's last-modified timestamp, all live URLs across all sites in your install, automatic exclusion of pages with search_indexed=False, and multi-site routing if you serve more than one root page.
Per-page sitemap control
Override get_sitemap_urls() on a Page subclass when you need finer control — e.g. to set changefreq dynamically based on publish cadence, or to expose multiple URLs per page (faceted category landing pages):
class ArticlePage(SeoMixin, Page): def get_sitemap_urls(self, request=None): return [{ "location": self.get_full_url(request), "lastmod": self.last_published_at, "changefreq": "weekly" if self.is_recent else "monthly", "priority": 0.8 if self.is_featured else 0.6, }]
Don't forget robots.txt
Tell crawlers where the sitemap lives — and stay opinionated about which bots you welcome. A reasonable starting robots.txt for a Wagtail site:
User-agent: * Allow: / Disallow: /admin/ Disallow: /django-admin/ Sitemap: https://example.com/sitemap.xml
055. Speed & Core Web Vitals
Page speed is a confirmed Google ranking signal and a strong correlate with conversion. The three Core Web Vitals are what Google publishes targets against. As of 2026 the thresholds are:
- LCP (Largest Contentful Paint): <2.5s — how fast the main content above the fold paints.
- INP (Interaction to Next Paint): <200ms — how responsive the page feels to user input.
- CLS (Cumulative Layout Shift): <0.1 — how much the layout shifts unexpectedly while loading.
Wagtail-specific speed wins
- Use
{% image %}withfill-andformat-webpoptions. Wagtail's image renditions generate appropriately-sized, modern-format versions of every uploaded image on demand. Never serve a 4MB hero image at thumbnail size. - Always set
widthandheighton images and iframes. Prevents CLS — the layout reserves the correct space before the asset loads. - Lazy-load below-the-fold images with
loading="lazy". Wagtail's{% image %}tag supports passing arbitrary attributes — wire this in your base template. - Cache aggressively at the right tier — Cloudflare or Fastly in front, Django page cache for anonymous traffic, fragment caching for expensive template sections. The patterns are documented in our Python web architecture patterns article.
- Preconnect to font and CDN origins. One extra
<link rel="preconnect">tag shaves 100–300ms off LCP on cold loads. - Serve fonts with
display: swap. Stops the dreaded invisible-text-then-flash that hurts both LCP and perceived performance. - WhiteNoise + Brotli in production. Compresses static assets ~25% better than gzip; sub-millisecond cost on the server.
Editorial speed levers
Some of the biggest speed wins are content choices, not engineering ones. Brief your editors:
- Maximum hero image weight (200KB compressed is a reasonable target after Wagtail's WebP conversion).
- Cap the number of embedded videos per page (iframes are expensive — consider a lite-YouTube embed pattern).
- Avoid third-party widgets that load on every page (tracking pixels, social embeds, chat widgets). One marketing-team-friendly widget can tank LCP for the whole site.
Every third-party script on your pages costs measurable LCP and INP. Audit them quarterly. If marketing can't articulate the conversion value of a script that adds 800ms to load time, remove it.
066. Structured data (JSON-LD)
Schema.org JSON-LD is the single biggest "easy win" for search visibility we see ignored on enterprise Wagtail sites. Add the right structured data and you unlock rich results — article headlines with author and date, FAQ accordions in search results, breadcrumbs above the title, organisation info in knowledge panels.
Wagtail makes this trivial because the data is already in your page models. You're just emitting it in the right format:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "{{ page.title|escapejs }}",
"datePublished": "{{ page.first_published_at|date:'c' }}",
"dateModified": "{{ page.last_published_at|date:'c' }}",
"author": { "@type": "Person", "name": "{{ page.author.name|escapejs }}" },
"publisher": { "@type": "Organization", "name": "{{ settings.core.SiteSettings.site_name|escapejs }}" },
{% if page.og_image %}
{% image page.og_image fill-1200x630 as ogimg %}
"image": "{{ ogimg.full_url }}",
{% endif %}
"mainEntityOfPage": "{{ page.get_full_url }}"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{% for ancestor in page.get_ancestors|slice:"1:" %}
{ "@type": "ListItem", "position": {{ forloop.counter }},
"name": "{{ ancestor.title|escapejs }}", "item": "{{ ancestor.full_url }}" },
{% endfor %}
{ "@type": "ListItem", "position": {{ page.get_ancestors|length }},
"name": "{{ page.title|escapejs }}", "item": "{{ page.get_full_url }}" }
]
}
</script>
The schemas worth implementing
Article/TechArticle/NewsArticle— every editorial page. Unlocks "Top Stories" eligibility and author/date in SERPs.BreadcrumbList— every non-home page. Replaces the URL in the SERP with a readable breadcrumb path.FAQPage— any page with a Q&A section. Often turns into an expandable accordion directly in search results.Organization— once, on the homepage. Powers the knowledge-panel rich result for branded searches.Product+Offer— for any ecommerce or pricing page. Price, availability and review stars show up in SERPs.Service— for service pages. Less SERP impact than Product but useful for B2B sites with structured offerings.
Validate every template with Google's Rich Results Test before launch and after any template change. A single trailing comma can invalidate the whole schema and lose the rich result.
077. Pre-launch SEO checklist
The list we walk through with marketing before any Wagtail site goes live:
- ✓ Every page type has an SEO mixin with overridable title, description, OG image and canonical
- ✓ Base template renders title, description, canonical, OG and Twitter tags consistently
- ✓ Robots meta tag is editor-controllable; defaults to
index, follow - ✓ Sitemap mounted at
/sitemap.xml, includeslastmodfrom page revisions - ✓
robots.txtpoints to sitemap, blocks admin paths only - ✓ 301 redirects in place for every legacy URL (use the Wagtail Redirects admin to manage post-launch)
- ✓ JSON-LD for
Article,Breadcrumb,Organizationvalidates in Google Rich Results Test - ✓ Core Web Vitals pass on mobile for the 5 highest-traffic templates (LCP <2.5s, INP <200ms, CLS <0.1)
- ✓ Images use
{% image %}with appropriatefill-spec; below-fold imagesloading="lazy" - ✓ Fonts use
display: swap; preconnect to font origin - ✓ Static assets served compressed (Brotli) and cached for ≥1 week (with versioned filenames if cached longer)
- ✓ Multi-language pages have
hreflangtags viawagtail-localize - ✓ Search Console + Bing Webmaster Tools verified; sitemap submitted; baseline coverage report captured
- ✓ Analytics (GA4 or Plausible) wired up with goal/event tracking for the routes that matter to SEO ROI
- ✓ HTTPS everywhere; HSTS with preload; mixed-content audit clean
- ✓ Accessibility checker passes (Axe-core) for every published template
If you can tick all 16, the site is starting from a position most competitors never reach.
088. Common questions
"Do we need wagtail-seo if we have the SeoMixin pattern above?"
Probably not on a small site. wagtail-seo is most valuable when you want sensible defaults for OG/Twitter/JSON-LD without thinking about every field, and when your editorial team is large enough that fewer "obvious" decisions on each page is a real win. For a single-product marketing site with one editor, the abstract mixin pattern is cleaner and gives you full control.
"How do we handle SEO for a headless Wagtail + Next.js setup?"
The Wagtail side stays the same — the SEO mixin still lives on each page and gets exposed via the REST or GraphQL API. The Next.js layer reads those fields and renders the <head> tags server-side. The sitemap and structured data move from Django templates into Next.js routes (or you can keep serving them from Wagtail and proxy them through). Our Wagtail development practice documents the full headless SEO pattern, and the headless CMS walkthrough shows the API plumbing.
"Will our SEO survive a WordPress → Wagtail migration?"
Yes, if it's done properly. Most clients see flat-to-improving Search Console impressions in the first quarter post-launch, with a measurable uplift inside six months thanks to faster Core Web Vitals and richer structured data. The risk is real but it's a project-execution risk, not a platform risk. The Wagtail migration blueprint details the redirect mapping, canonical preservation and sitemap continuity we apply on every migration.
"How often should we re-audit our Wagtail SEO setup?"
Quarterly for technical SEO (Core Web Vitals drift, broken redirects from content moves, schema validation regressions); monthly for content SEO (Search Console queries, click-through rates, new ranking opportunities). The technical audit is roughly half a day if you've followed the patterns above — most issues are detected automatically and pre-empted.
"What about AI overviews and answer engines?"
The same structural choices that win on Google's traditional SERPs also win for AI overviews and emerging answer engines: clean HTML, accurate structured data, fast pages, and an llms.txt file that surfaces your content map for AI crawlers. ChatGPT, Claude, Perplexity and Google's own AI overviews disproportionately cite pages that make their content easy to parse — exactly the things this guide optimises for.
If you'd like a sanity check on your Wagtail SEO setup or a structured improvement plan, the 2-week Discovery Sprint covers exactly this — technical audit, prioritised remediation, and a written roadmap your marketing team can act on.