Skip to main content

wagtail-herald

PyPI version Downloads CI codecov License: BSD-3-Clause Published on Django Packages

Philosophy

SEO optimization shouldn't require deep technical knowledge. While Wagtail provides excellent content management, setting up proper meta tags, Open Graph, Twitter Cards, and Schema.org structured data requires significant manual work.

wagtail-herald provides a comprehensive SEO solution with just two template tags. Site-wide settings are managed through Wagtail's admin interface, while page-specific SEO can be configured per-page with sensible defaults.

The goal is to help content editors achieve best-practice SEO without touching code, while giving developers full control when needed.

Key Features

  • Simple Integration - Just 3 template tags: {% seo_head %}, {% seo_body %}, {% seo_schema %}
  • Site-wide Settings - Configure Organization, favicons, social profiles from admin
  • Page-level SEO - Uses Wagtail's built-in SEO fields + OG image override
  • 13+ Schema Types - Article, Product, FAQ, Event, LocalBusiness, and more
  • Automatic BreadcrumbList - Generated from page hierarchy
  • Locale Support - Per-page language/region targeting with {% page_lang %} tag
  • Google Tag Manager - GTM integration with noscript fallback
  • robots.txt Management - Configure robots.txt from admin interface
  • ads.txt Management - Configure ads.txt (Authorized Digital Sellers) from admin interface
  • security.txt Management - Configure security.txt (RFC 9116) from admin interface
  • Custom Code Injection - Add custom HTML to head and body from admin
  • Japanese UI - Full Japanese localization for admin interface

Installation

pip install wagtail-herald

Add to your INSTALLED_APPS:

# settings.py
INSTALLED_APPS = [
    # ...
    'wagtail.contrib.settings',  # Required
    'wagtail_herald',
    # ...
]

Quick Start

1. Add Template Tags

{% load wagtail_herald %}
<!DOCTYPE html>
<html lang="{% page_lang %}">
<head>
    {% seo_head %}
</head>
<body>
    {% seo_body %}

    <!-- Your content -->

    {% seo_schema %}
</body>
</html>

That's it! The template tags handle everything:

  • {% seo_head %} - Meta tags, OG, Twitter Card, favicon, GTM script, custom head HTML
  • {% seo_body %} - GTM noscript fallback, custom body end HTML
  • {% seo_schema %} - All JSON-LD structured data
  • {% page_lang %} - Language code for html lang attribute

2. Configure Site Settings

Go to Settings > SEO Settings in Wagtail admin to configure:

  • Organization name, logo, type
  • Social media profiles (Twitter, Facebook)
  • Default OG image and locale
  • Favicon and Apple Touch Icon
  • Google Tag Manager (GTM)
  • robots.txt content
  • Custom head/body HTML injection

3. Add SEO Mixin to Pages (Optional)

For page-level SEO control, add the mixin to your page models:

from wagtail.models import Page
from wagtail_herald.models import SEOPageMixin

class ArticlePage(SEOPageMixin, Page):
    # Your fields...

    content_panels = Page.content_panels + [
        # Your panels...
    ]

    promote_panels = Page.promote_panels + SEOPageMixin.seo_panels

This adds an "SEO" panel in the page editor with:

  • OG image override
  • Schema type selector (Article, Product, FAQ, etc.)
  • Locale selector (ja_JP, en_US, en_GB, etc.)
  • noindex/nofollow options
  • Canonical URL override

Note: For SEO title and meta description, use Wagtail's built-in seo_title and search_description fields in the Promote tab. The template tags automatically use these fields.

4. Override SEO per Sub-route (RoutablePageMixin)

When using RoutablePageMixin, individual sub-routes often represent different content items that need their own SEO metadata. Pass well-known keys through context_overrides and {% seo_head %} will use them instead of the page object's fields.

Supported context keys:

Key Overrides
seo_title Page title and OG/Twitter title
seo_description Meta description and OG/Twitter description
seo_canonical_url Canonical URL and og:url
seo_og_image OG image and Twitter image (Wagtail Image instance)
from wagtail.contrib.routable_page.models import RoutablePageMixin, path
from wagtail.models import Page

class BlogIndexPage(RoutablePageMixin, Page):

    @path("<slug:slug>/")
    def detail(self, request, slug):
        item = get_object_or_404(BlogPost, slug=slug)
        return self.render(request, context_overrides={
            "seo_title": item.name,
            "seo_description": item.summary,
            "seo_canonical_url": request.build_absolute_uri(),
        })

Any key not provided falls back to the page object's own fields, so you only need to specify what differs per sub-route.

Supported Schema Types

Site-wide (Automatic)

  • WebSite - Site search box support
  • Organization - Company/organization info

Page-selectable

Type Use Case
WebPage General pages (default)
Article General articles
NewsArticle News content
BlogPosting Blog posts
Product Product pages
LocalBusiness Store/business info
Service Service descriptions
FAQPage FAQ pages
HowTo How-to guides
Event Events
Person Profile pages
Recipe Recipes
Course Online courses
JobPosting Job listings

Automatic

  • BreadcrumbList - Generated from page hierarchy

Output Example

{% seo_head %} Output

<!-- Basic Meta -->
<title>Page Title</title>
<meta name="description" content="Page description...">
<meta name="robots" content="index, follow">

<!-- Canonical -->
<link rel="canonical" href="https://example.com/page/">

<!-- hreflang (multilingual) -->
<link rel="alternate" hreflang="ja" href="https://example.com/ja/page/">
<link rel="alternate" hreflang="en" href="https://example.com/en/page/">
<link rel="alternate" hreflang="x-default" href="https://example.com/ja/page/">

<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/media/favicon.svg">
<link rel="icon" type="image/png" sizes="48x48" href="/media/favicon.png">
<link rel="apple-touch-icon" sizes="180x180" href="/media/apple-touch-icon.png">

<!-- Open Graph -->
<meta property="og:type" content="article">
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Page description...">
<meta property="og:image" content="https://example.com/media/og-image.jpg">
<meta property="og:url" content="https://example.com/page/">
<meta property="og:site_name" content="Site Name">

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@handle">
<meta name="twitter:title" content="Page Title">
<meta name="twitter:description" content="Page description...">
<meta name="twitter:image" content="https://example.com/media/og-image.jpg">

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');</script>

<!-- Custom Head HTML -->
<script src="https://example.com/custom.js"></script>

{% seo_body %} Output

<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>

<!-- Custom Body End HTML -->
<script src="https://widget.example.com/chat.js"></script>

{% seo_schema %} Output

<script type="application/ld+json">
[
  {
    "@context": "https://schema.org",
    "@type": "WebSite",
    "name": "Site Name",
    "url": "https://example.com/"
  },
  {
    "@context": "https://schema.org",
    "@type": "Organization",
    "name": "Company Name",
    "url": "https://example.com/",
    "logo": "https://example.com/media/logo.png",
    "sameAs": [
      "https://twitter.com/company",
      "https://www.facebook.com/company"
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {"@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/"},
      {"@type": "ListItem", "position": 2, "name": "Blog", "item": "https://example.com/blog/"},
      {"@type": "ListItem", "position": 3, "name": "Article Title"}
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Article Title",
    "image": ["https://example.com/media/article-image.jpg"],
    "author": {"@type": "Person", "name": "Author Name"},
    "datePublished": "2025-01-15T10:00:00+09:00",
    "dateModified": "2025-01-16T15:30:00+09:00"
  }
]
</script>

Configuration

All settings are optional and configured through Wagtail admin:

Site Settings (Admin UI)

Setting Description
Organization name Company/site name for Schema
Organization type Corporation, LocalBusiness, Person, etc. (use Person for individual/personal sites)
Organization logo Logo image for Schema
Twitter handle @username (without @)
Facebook URL Facebook page URL
Default locale Default og:locale (e.g., en_US, ja_JP)
Default OG image Fallback image for social sharing (1200x630)
Favicon (SVG) SVG favicon for modern browsers (recommended)
Favicon (PNG) PNG fallback, minimum 48x48 (Google requirement)
Apple Touch Icon iOS home screen icon (180x180)
GTM Container ID Google Tag Manager (GTM-XXXXXX)
robots.txt content Custom robots.txt content
ads.txt content Authorized Digital Sellers declaration (returns 404 if empty)
security.txt content Security vulnerability reporting info per RFC 9116 (returns 404 if empty)
Custom head HTML Custom HTML for <head> section
Custom body end HTML Custom HTML before </body> (chat widgets, etc.)

Django Settings (Optional)

# settings.py
WAGTAIL_HERALD = {
    # Default robots meta (can be overridden per-page)
    'DEFAULT_ROBOTS': 'index, follow',

    # OG image rendition filter (1200x630 is optimal for social sharing)
    'OG_IMAGE_FILTER': 'fill-1200x630',

    # Favicon rendition filter (48x48 minimum recommended by Google)
    'FAVICON_FILTER': 'fill-48x48',
}

Locale Support

wagtail-herald provides per-page language and region targeting for mixed-language content.

Use Case: Mixed Language Content

Write Japanese and English articles on the same site by selecting locale per page:

{% load wagtail_herald %}
<!DOCTYPE html>
<html lang="{% page_lang %}">
<head>
    {% seo_head %}
    <!-- Outputs: <meta property="og:locale" content="ja_JP"> -->
</head>
  • {% page_lang %} - Returns language code (e.g., ja, en)
  • {% page_locale %} - Returns full locale (e.g., ja_JP, en_US)
  • {% seo_head %} - Automatically includes og:locale meta tag

Available Locales

Locale Language
ja_JP 日本語 (日本)
en_US English (US)
en_GB English (UK)
zh_CN 中文 (简体)
zh_TW 中文 (繁體)
ko_KR 한국어
fr_FR Français (France)
de_DE Deutsch (Deutschland)
es_ES Español (España)
pt_BR Português (Brasil)

Set the default locale in Settings > SEO Settings, then override per-page using the SEOPageMixin.

robots.txt Management

Configure robots.txt from Wagtail admin without editing files.

Setup

Add wagtail-herald URLs to your urls.py:

from django.urls import include, path

urlpatterns = [
    path('', include('wagtail_herald.urls')),
    # ... other urls
]

Configuration

Go to Settings > SEO Settings and edit the robots.txt content:

User-agent: *
Allow: /
Disallow: /admin/
Disallow: /search/

Sitemap: https://example.com/sitemap.xml

If left empty, a sensible default is generated (allow all crawlers, include sitemap URL).

ads.txt Management

Configure ads.txt (Authorized Digital Sellers) from Wagtail admin without editing files.

Setup

Include wagtail-herald URLs in your urls.py (same as robots.txt):

from django.urls import include, path

urlpatterns = [
    path('', include('wagtail_herald.urls')),
    # ... other urls
]

Configuration

Go to Settings > SEO Settings and edit the ads.txt content:

google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0

Unlike robots.txt, if the ads.txt field is left empty, the /ads.txt endpoint returns a 404 response rather than generating default content.

security.txt Management

Configure security.txt (RFC 9116) from Wagtail admin without editing files. This file helps security researchers report vulnerabilities through the standardized /.well-known/security.txt path.

Setup

Include wagtail-herald URLs in your urls.py (same as robots.txt and ads.txt):

from django.urls import include, path

urlpatterns = [
    path('', include('wagtail_herald.urls')),
    # ... other urls
]

Configuration

Go to Settings > SEO Settings and edit the security.txt content. Per RFC 9116, the Contact and Expires fields are required:

Contact: mailto:security@example.com
Expires: 2026-12-31T23:59:59z
Preferred-Languages: en, ja
Canonical: https://example.com/.well-known/security.txt

If the security.txt field is left empty, the /.well-known/security.txt endpoint returns a 404 response.

Requirements

Python Django Wagtail
3.10+ 4.2, 5.1, 5.2 6.4, 7.0, 7.2

Documentation

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

BSD 3-Clause License. See LICENSE for details.

Release files for wagtail-herald 0.12.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wagtail-herald 0.12.1
File Size Uploaded
wagtail_herald-0.12.1.tar.gz 149.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wagtail-herald 0.12.1
File Interpreter ABI Platform
wagtail_herald-0.12.1-py3-none-any.whl Python 3 none any Details

Total release size: 264.1 kB

Release files / wagtail_herald-0.12.1.tar.gz

Download URL wagtail_herald-0.12.1.tar.gz
Size 149.9 kB
Tags Source
SHA-256 checksum
How to use checksums
bc32b9d778f7074e330e0d80d3812c1b410c16d5b2ff3056587d13ea9cc5e3a0
BLAKE2b-256 checksum
How to use checksums
792385ae4b09e8ef9dfdc2ac3fa615ecd2a6e25aac14274a7f52ff6102417c18
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.

Transparency log

Release files / wagtail_herald-0.12.1-py3-none-any.whl

Download URL wagtail_herald-0.12.1-py3-none-any.whl
Size 114.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c74d773ab7e7e1412c2248504aef3e6363a88f8d4747caedbcca5bf824a63e3
BLAKE2b-256 checksum
How to use checksums
aa29a028e814113321a2548d23e7adf75045c12d07ac51e61015095843a61a63
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.12.1 This release

2 release files

0.11.1

2 release files

0.11.0

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page