Guide: Blog posts and articles¶
This guide covers the options for attaching SEO metadata to a model you own, from zero-configuration conventions through to per-row, admin-editable fields.
Level 1 — conventional field names (no configuration)¶
If your model already has fields named title, description (or summary or
excerpt), and image (or cover_image, photo, thumbnail), and a
get_absolute_url() method, SeoModelMixin picks them up with no further
configuration:
from django.db import models
from seo_suite.mixins import SeoModelMixin
class Article(SeoModelMixin, models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
image = models.ImageField(upload_to="articles/", blank=True)
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return f"/articles/{self.slug}/"
The full convention lookup order:
| SEO field | Tries, in order |
|---|---|
title |
title, name, headline |
meta_description |
meta_description, description, summary, excerpt |
meta_keywords |
meta_keywords, keywords |
og_image |
og_image, image, cover_image, photo, thumbnail |
canonical_url |
return value of get_absolute_url() |
For ImageField and FileField values, the mixin automatically calls .url
when a file is uploaded; if the field is blank it contributes nothing.
A blank text field ("") also contributes nothing. It is treated the same as
a missing field, so a lower-priority default can still fill the gap.
Level 2 — custom field names with SEO_FIELD_MAP¶
When your model's field names do not match the conventions, declare
SEO_FIELD_MAP as a class attribute:
class Article(SeoModelMixin, models.Model):
headline = models.CharField(max_length=200)
blurb = models.TextField(blank=True)
cover = models.ImageField(upload_to="covers/", blank=True)
slug = models.SlugField(unique=True)
SEO_FIELD_MAP = {
"title": "headline",
"meta_description": "blurb",
"og_image": "cover",
}
def get_absolute_url(self):
return f"/articles/{self.slug}/"
SEO_FIELD_MAP values are attribute names on the model. The mixin also accepts
method names (it calls zero-argument callables for you).
You can map any SeoMetadata field: title, h1, meta_description,
meta_keywords, og_image, robots, canonical_url.
Level 3 — per-row editor-editable SEO columns¶
When your editorial team needs to customise SEO for individual articles without
touching code, use SeoModelFieldsMixin. It is an abstract model that adds
seo_* columns directly to your table:
from seo_suite.mixins import SeoModelFieldsMixin
class Article(SeoModelFieldsMixin, models.Model):
headline = models.CharField(max_length=200)
blurb = models.TextField(blank=True)
cover = models.ImageField(upload_to="covers/", blank=True)
slug = models.SlugField(unique=True)
SEO_FIELD_MAP = {
"title": "headline",
"meta_description": "blurb",
"og_image": "cover",
}
def get_absolute_url(self):
return f"/articles/{self.slug}/"
SeoModelFieldsMixin inherits from SeoModelMixin, so all conventions and
SEO_FIELD_MAP still work. It then merges a higher-priority layer from its own
columns:
| Column | Overrides |
|---|---|
seo_title |
title |
seo_description |
meta_description |
seo_keywords |
meta_keywords |
seo_robots |
robots |
seo_canonical |
canonical_url |
seo_og_image |
og_image |
An empty seo_title column means "no opinion" — the convention fallback
(headline via SEO_FIELD_MAP) is used instead. This means editors only need
to fill in columns when they want to deviate from the automatic value.
Because SeoModelFieldsMixin is abstract, you create the migration:
Registering the fields in Django Admin¶
# blog/admin.py
from django.contrib import admin
from .models import Article
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
fieldsets = (
(None, {
"fields": ("headline", "blurb", "cover", "slug"),
}),
("SEO", {
"fields": (
"seo_title",
"seo_description",
"seo_keywords",
"seo_robots",
"seo_canonical",
"seo_og_image",
),
"classes": ("collapse",),
"description": (
"Leave blank to use the article's headline and blurb automatically."
),
}),
)
Wiring to a detail view¶
# blog/views.py
from django.views.generic import DetailView
from seo_suite.mixins import SeoViewMixin
from .models import Article
class ArticleDetail(SeoViewMixin, DetailView):
model = Article
slug_field = "slug"
slug_url_kwarg = "slug"
The view passes self.object to the resolver automatically. You do not call
get_seo_metadata() yourself.
Adding JSON-LD Article schema¶
Add SEO_SCHEMA_PROFILES to emit an Article structured-data block alongside
the meta tags:
class Article(SeoModelMixin, models.Model):
headline = models.CharField(max_length=200)
blurb = models.TextField(blank=True)
cover = models.ImageField(upload_to="covers/", blank=True)
slug = models.SlugField(unique=True)
published_at = models.DateTimeField()
updated_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey("auth.User", on_delete=models.SET_NULL, null=True)
SEO_FIELD_MAP = {
"title": "headline",
"meta_description": "blurb",
"og_image": "cover",
}
SEO_SCHEMA_PROFILES = ["Article", "BreadcrumbList"]
def get_absolute_url(self):
return f"/articles/{self.slug}/"
def get_breadcrumbs(self):
return [
("Home", "/"),
("Articles", "/articles/"),
(self.headline, self.get_absolute_url()),
]
The Article profile reads headline, blurb, cover.url,
published_at, updated_at, and author automatically, matching the field
names above. See JSON-LD structured data for a full profile
reference.
Overriding specific fields at the view level¶
Use view-level attributes when you want to force a particular SEO value regardless of the object — for example, a fixed robots directive for draft content:
The view layer (priority 50) always wins over the object layer (priority 40). See Detail views for more patterns.