Making Wagtail pages more SEO friendly with Wagtail Metadata

Introduction

Wagtail pages are great for creating a lot of rich content straight out of the box, but for SEO optimization, they need some tweaking.

Here, I subclass the Page model with some help from the wagtail-metadata plug-in.

This sub-classed model becomes the base for all site pages and holds all the data for og metadata, twitter cards, page description etc..

Creating the SEO Page Model

The metadata tags are the main focus here, but we'll also add two new fields to handle page descriptions and titles both on-site and off, with some pre-save validation to check that both are optimal lengths for SEO.

Installing wagtail-metadata

Begin by installing wagtail-metadata:

pip install wagtail-metadata

Add 'wagtailmetadata' to your installed apps.

The plugin provides various mixins to use. Source code and documentation are available on GitHub:

GitHub - neon-jungle/wagtail-metadata: A tool to assist with metadata for social media and search engines.

A tool to assist with metadata for social media and search engines. - GitHub - neon-jungle/wagtail-metadata: A tool to assist with metadata for social media and search engines.

The title, image and description in the above block have been scraped directly from the metadata of that page. I'll show how's that's done in a later blog, but for now, what the wagtail-metadata plug-in will do is allow the same to be created for any page you create.

To see it in action, copy the URL of this page and paste it into a new post on a dynamic site such as Facebook. You'll see the preview card without needing to submit the post.

Creating the mixin

I'm using a slightly modified version of the WagtailImageMetadataMixin class to make a new mixin.

I generally keep critical site-wide functionality like this in a separate core app. I'll have all my site settings, hooks and other customisations here as much as is practical.

Delete the pre-3.0 imports if you've already updated.

# core.models.py

from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from wagtailmetadata.models import WagtailImageMetadataMixin
if WAGTAIL_VERSION < (3, 0):
    from wagtail.admin.edit_handlers import FieldPanel, MultiFieldPanel
    from wagtail.core.models import Page
    from wagtail.images.edit_handlers import ImageChooserPanel
else:
    from wagtail.admin.panels import FieldPanel, MultiFieldPanel
    from wagtail.models import Page

def get_image_model_string():
    try:
        image_model = settings.WAGTAILIMAGES_IMAGE_MODEL
    except AttributeError:
        image_model = 'wagtailimages.Image'
    return image_model

class SEOPageMixin(WagtailImageMetadataMixin, models.Model):
    search_image = models.ForeignKey(
        get_image_model_string(),
        null=True,
        blank=False,
        related_name='+',
        on_delete=models.SET_NULL,
        verbose_name=_('Search Image'),
        help_text=_("The image to use on previews of this page on external links and search results. This will also be the image used for blog posts on the index pages.")
    )

    summary = models.TextField(
        null=False,
        blank=False,
        help_text=_("A summary of the page to be used on index pages. If Meta Description is left blank, this text will be used on search results and link previews.")
    )

    content_panels = Page.content_panels + [
        FieldPanel('summary'),
    ]
    
    promote_panels = [
        MultiFieldPanel([
            FieldPanel('slug'),
            ImageChooserPanel('search_image'),
            FieldPanel('seo_title'),
            FieldPanel('search_description'),
            FieldPanel('show_in_menus'),
        ], _('Common page configuration')),
    ]

    def get_meta_url(self):
        return self.full_url

    def get_meta_title(self):
        return self.seo_title or self.title

    def get_meta_description(self):
        return self.search_description or self.summary

    def get_meta_image(self):
        return self.search_image

    class Meta:
        abstract = True

    def clean(self):
        super().clean()
        errors = {}

        len_search_title = len(self.seo_title or self.title)
        if len_search_title < 15 or len_search_title > 70:
            if len_search_title==0:
                msg = _("empty")
            else:
                msg = _(f"{len_search_title} character{'s' * bool(len_search_title>1)}")
            if self.seo_title:
                errors['seo_title'] = ErrorList([_(f'Title tag is {msg}. It should be between 15 and 70 characters for optimum SEO.')])
            else:
                errors['seo_title'] = ErrorList([_(f'Page title is {msg}. Create a title tag between 15 and 70 characters for optimum SEO.')])

        len_search_description = len(self.search_description or self.summary)
        if len_search_description < 50 or len_search_description > 160:
            if len_search_description==0:
                msg = _("empty")
            else:
                msg = _(f"{len_search_description} character{'s' * bool(len_search_description>1)}")
            if self.search_description:
                errors['search_description'] = ErrorList([_(f'Meta Description is {msg}. It should be between 50 and 160 characters for optimum SEO.')])
            else:
                errors['search_description'] = ErrorList([_(f'Summary is {msg}. Create a meta description between 50 and 160 characters for optimum SEO.')])

        if errors:
            raise ValidationError(errors)

The SEOPage mixin creates 'page summary' and 'search image' fields and adds them to the Content and Promote panels respectively.

The get_meta methods will be used later in the template to render all the metadata:

  • The meta title will come from the optional Title Tag (seo_title) on the Promote tab if filled in, otherwise from the page title.
  • The meta description will come from the optional Meta Description (search_description) on the Promote tab if filled in, otherwise the new Page Summary field will be used.

It's a good idea to use the Title Tag and Meta Description fields on the promote tab.

The Title Tag gives you the opportunity to add in common modifiers that can help in ranking (such as How to, Review, Best, Tips, Top, Find, Buy, Free, Easy) that you might not want to appear in the content title.

Summary provides an opportunity to give a meaningful page brief in your website (such as internal search results and product or blog page listing cards), while Meta Description allows you to enter a more brief, more Search Engine friendly summation - this becomes the description snippet on Google for instance.

Finally, the custom clean() method provides a check to make sure the title and description are optimised for search engines:

  • SEO title should be between 15 and 70 characters. The code checks the length of seo_title if present, otherwise the page title length is validated.
  • SEO description should be between 50 and 160 characters. Similarly, the length is validated from search_description if present, otherwise summary is checked.

Adding the SEOPage abstract class

We're not quite there as all we have so far is the abstract mixin class. We need to make the new SEOPage abstract class from this:

class SEOPage(SEOPageMixin, Page):

    search_fields = Page.search_fields + [
        index.SearchField('summary'),
    ]

    class Meta:
        abstract = True

Note that I'm adding the new summary field to the internal search index. This is entirely optional and can be replaced with a simple pass if not desired.

Your page models should all now inherit SEOPage (either directly or indirectly) so that, for example, your home page model might look like:

class HomePage(SEOPage):

    --- class property definition ---

    content_panels = SEOPage.content_panels + [
        --- class panels definition ---
 ]

Templates

Adding the metadata

Finally, in the <head> section of your base.html (or in your head.html if you configured your site this way), load seo_metadata_tags and the appropriate code to place all of your metadata.

{%load static seo_metadata_tags%}
....
  {%if request is not None%} {#500 error has no request#}
    {%meta_tags as meta_tags%}
    {%if meta_tags is not None%}{{meta_tags}}{%endif%}
  {%endif%}
....

For this site (and others), I added a few more tags that may or may not help with ranking - the shifting sands of Google algorithms might have made some of these obsolete already, but they're included anyway:

<meta property="og:type" content="website"/>
<meta name="robots" content="index, follow, archive, imageindex, odp, snippet, translate, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<meta name="target" content="all"/>
<meta name="audience" content="all"/>
<meta name="coverage" content="Worldwide"/>
<meta name="distribution" content="Global">
<meta name="rating" content="safe for kids"/>

og:type is wanted by Facebook in case you're using something like Facebook OAuth (as this site is), along with

<meta property="fb:app_id" content="fb-app-ID" />

The meta name="robots" supplies directives to search engines on how your page should be crawled. You could also supply this on a per-page basis if necessary. Google Developers has a good article describing the use of this.

I suspect the others in the list are redundant these days, but there is nothing that will impact SEO negatively there.

The rendered meta tags for this page then becomes:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="index, follow, archive, imageindex, odp, snippet, translate, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="How to Make Wagtail Pages More SEO Friendly with Wagtail Metadata">
<meta name="twitter:description" content="Use sub-classing to create SEO optimized og metadata, twitter cards and page description for your Wagtail site with the wagtail-metadata plug-in.">
<meta name="twitter:image" content="https://enzedonline.com/media/images/adding_wagtail_seo_metadata.original.png">
<meta property="og:url" content="https://enzedonline.com/en/tech-blog/making-wagtail-pages-more-seo-friendly-with-wagtail-metadata/" />
<meta property="og:title" content="How to Make Wagtail Pages More SEO Friendly with Wagtail Metadata" />
<meta property="og:description" content="Use sub-classing to create SEO optimized og metadata, twitter cards and page description for your Wagtail site with the wagtail-metadata plug-in." />
<meta property="og:site_name" content="Enzed Online" />
<meta property="og:image" content="https://enzedonline.com/media/images/adding_wagtail_seo_metadata.original.png" />
<meta property="og:image:width" content="688" />
<meta property="og:image:height" content="272" />
<meta itemprop="url" content="https://enzedonline.com/en/tech-blog/making-wagtail-pages-more-seo-friendly-with-wagtail-metadata/"/>
<meta itemprop="name" content="How to Make Wagtail Pages More SEO Friendly with Wagtail Metadata">
<meta itemprop="description" content="Use sub-classing to create SEO optimized og metadata, twitter cards and page description for your Wagtail site with the wagtail-metadata plug-in." />
<meta itemprop="image" content="https://enzedonline.com/media/images/adding_wagtail_seo_metadata.original.png" />
<title>How to Make Wagtail Pages More SEO Friendly with Wagtail Metadata</title>
<meta name="description" content="Use sub-classing to create SEO optimized og metadata, twitter cards and page description for your Wagtail site with the wagtail-metadata plug-in.">
<meta property="og:type" content="website"/>
<meta name="target" content="all"/>
<meta name="audience" content="all"/>
<meta name="coverage" content="Worldwide"/>
<meta name="distribution" content="Global">
<meta name="rating" content="safe for kids"/>

Setting the Image Filter

This is an undocumented but very useful feature of wagtail-metadata that I found out about only after picking through the code to see if I could do something else.

The default is to serve up your search_image full size. If, for some reason, your search image has been uploaded at 3000x4000px, then this is what is getting served each time a page requests your open graph summary for instance. That's quite a bit of overhead.

In your site settings, you can assign an image filter to the site settings variable WAGTAILMETADATA_IMAGE_FILTER which will create a formatted rendition and set this to your image tags above. A good idea if your search_image is large format or not a suitable aspect ratio.

Image filter in this case means a string that you would supply to image.getrendition() which takes all the usual operations you might use with the {%image%} template tag, with the difference that multiple operations are separated by the pipe | character rather than spaces.

If you're interested in making a custom filter, I go through creating thumbnails with preserved images (i.e. resized without cropping or stretching) in another article.

Most uses for the image seem to be expecting a 2:1 format, so I set the filter to use my custom thumbnail operation to create an 800x400px image and output to png.

WAGTAILMETADATA_IMAGE_FILTER = "thumbnail-800x400|format-png"

Bypass Analytics for Unpublished Content

While we're in the <head> section and talking about SEO, this is a good time to make sure your dev site and page previews aren't adding to your Google Analytics page views with a check for either debug mode or request.is_preview:

{%if not debug and not request.is_preview%}
    <!-- Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id={{settings.site_settings.Tokens.google_analytics}}"></script>
    <script src="{%static 'js/analytics.js'%}"></script>
{%endif%}

Be sure to have your internal IP addresses added to INTERNAL_IPS in your settings file. If you're only running your development site locally, then you'll just need:

INTERNAL_IPS = ['127.0.0.1',]

Summary

From the perspective of site development, there's obviously a lot more that can be done to boost search engine rankings. If you're interested in boosting your search engine rankings further, SE Ranking has some very useful blog posts on the subject. It's a dark art that needs constant adaptation as search engines tweak their algorithms.

In this blog, we created a page model with some SEO building blocks, including generating dynamic metadata, adding a search page and preview card image and ensuring the search engine title and description meet the recommended sizes.


  Please feel free to leave any questions or comments below, or send me a message here