Guide: JSON-LD structured data¶
django-seo-suite ships a library of ready-made schema.org profiles. Enable
them with a single class attribute, with no template editing required.
Enabling profiles on a model¶
from django.db import models
from seo_suite.mixins import SeoModelMixin
class Article(SeoModelMixin, models.Model):
headline = models.CharField(max_length=200)
blurb = models.TextField(blank=True)
cover = models.ImageField(upload_to="covers/", blank=True)
published_at = models.DateTimeField()
updated_at = models.DateTimeField(auto_now=True)
slug = models.SlugField(unique=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()),
]
{% seo_head %} renders each profile as a separate <script
type="application/ld+json"> block.
Free profile library¶
All profiles below are registered by default. Reference them by key in
SEO_SCHEMA_PROFILES (on a model) or seo_schema_profiles (on a view).
WebPage¶
Key: "WebPage"
Reads title/name/headline → name, description/summary/excerpt →
description, get_absolute_url() → url. Always emitted (even with no
matching fields).
WebSite¶
Key: "WebSite"
Reads SEO_SUITE["DEFAULTS"]["og"]["site_name"] → name, request root →
url. Typically added to a homepage view.
Organization¶
Key: "Organization"
Reads SEO_SUITE["DEFAULTS"]["og"]["site_name"] → name, request root →
url. Pair with WebSite on a homepage.
Person¶
Key: "Person"
Reads full_name/name/get_full_name() → name, get_absolute_url() →
url. Use on author profile pages.
BreadcrumbList¶
Key: "BreadcrumbList"
Reads get_breadcrumbs() (or a breadcrumbs attribute). The method must
return a list of (name, url) tuples or a list of dicts with "name" and
"url" keys:
def get_breadcrumbs(self):
# Tuple form:
return [
("Home", "/"),
("Articles", "/articles/"),
(self.headline, self.get_absolute_url()),
]
# Dict form (equivalent):
return [
{"name": "Home", "url": "/"},
{"name": "Articles", "url": "/articles/"},
{"name": self.headline, "url": self.get_absolute_url()},
]
Emitted only when at least one valid item is present.
Article¶
Key: "Article"
| JSON-LD field | Source attributes tried |
|---|---|
headline |
headline, title, name |
description |
meta_description, description, summary, excerpt |
image |
og_image, image, cover_image, thumbnail |
datePublished |
date_published, published_at, published, pub_date, created |
dateModified |
date_modified, modified, updated_at, updated |
author |
author_name, author (wrapped as {"@type":"Person","name":...}) |
mainEntityOfPage |
get_absolute_url() |
FAQPage¶
Key: "FAQPage"
Reads get_faqs() (or a faqs attribute). The method must return a list of
(question, answer) tuples or dicts with "question"/"answer" keys (also
accepts "q"/"a" shorthand):
def get_faqs(self):
return self.faq_items.values_list("question", "answer")
# or:
return [
{"question": "What is …?", "answer": "It is …"},
{"q": "How do I …?", "a": "You can …"},
]
Emitted only when at least one complete question/answer pair is present.
Product¶
Key: "Product"
| JSON-LD field | Source attributes tried |
|---|---|
name |
name, title |
description |
meta_description, description, summary |
image |
og_image, image, cover_image |
sku |
sku |
brand |
brand (wrapped as {"@type":"Brand","name":...}) |
offers |
Built from price, currency/price_currency, availability |
Overriding profile fields with get_schema_data¶
Any model can supply or override specific fields for a profile without subclassing it:
class Article(SeoModelMixin, models.Model):
headline = models.CharField(max_length=200)
SEO_SCHEMA_PROFILES = ["Article"]
def get_schema_data(self, key, context=None):
if key == "Article":
return {
"inLanguage": "en-US",
"publisher": {
"@type": "Organization",
"name": "My Blog",
"logo": {"@type": "ImageObject", "url": "https://example.com/logo.png"},
},
}
return {}
The dict returned by get_schema_data is merged on top of the profile's
automatic output (your keys win over the profile's keys). Return {} or
None for profiles you do not want to modify.
Rich images with get_schema_image¶
By default a profile's image field is a bare URL string (resolved from
og_image, image, cover_image, thumbnail). When you also have the
full-size original and its dimensions, you can emit a schema.org
ImageObject instead — Google prefers
images with explicit dimensions for rich results.
Implement the optional get_schema_image(key, context) hook. Whatever it
returns is used verbatim as the profile's image value — a string, an
ImageObject dict, or a list of either. django-seo-suite does not process
your images; map your own thumbnail library (easy-thumbnails, sorl, imagekit,
…) however you like:
class Article(SeoModelMixin, models.Model):
cover = models.ImageField(upload_to="covers/")
SEO_SCHEMA_PROFILES = ["Article", "BreadcrumbList"]
def get_schema_image(self, key, context=None):
# The `key` argument is a per-profile switch: return a value only
# for the profiles you want, and None to fall back / skip.
if key != "Article":
return None
thumb = get_thumbnailer(self.cover)["card"] # your thumbnail backend
return {
"@type": "ImageObject",
"contentUrl": self.cover.url,
"thumbnailUrl": thumb.url,
"width": self.cover.width,
"height": self.cover.height,
}
Resolution order for the image field: the convention bare-string is set
first, then get_schema_image overrides it, then get_schema_data (above)
wins last if it also returns an image key.
The hook is entirely optional — without it, behaviour is unchanged. The
key argument lets one method serve every profile while you decide which ones
carry an image (return None to skip).
Caching on high-traffic sites
get_schema_image runs at resolve time, so a heavy hook (one that hits
a thumbnail backend or probes image dimensions) is called once per request
only. Enable the object resolution cache (SEO_SUITE["CACHE_TTL"] > 0,
which requires a configured Django cache backend) and the hook's output is
stored as part of the cached metadata and reused across requests —
invalidated automatically when the object is saved or deleted. With caching
off (the default), the hook runs on every resolve, so keep it cheap or cache
inside it.
Enabling profiles on a view¶
Use seo_schema_profiles (note: no SEO_ prefix, lowercase) on a view:
class HomepageView(SeoViewMixin, TemplateView):
template_name = "home.html"
seo_title = "My Site"
seo_schema_profiles = ["WebSite", "Organization"]
Site-wide default profiles¶
To add a profile to every page on the site, use DEFAULT_SCHEMA_PROFILES:
Profiles from DEFAULT_SCHEMA_PROFILES are merged alongside per-model and
per-view profiles; JSON-LD blocks from all sources accumulate.
How JSON-LD accumulates¶
Unlike scalar fields where higher priority wins, JSON-LD blocks accumulate
across all layers. A model that declares ["Article"] and a view that
declares ["WebPage"] will emit both blocks on the same page. This is
intentional: multiple structured-data types on one page is valid and often
desirable.
Custom serializer¶
By default the serializer escapes <, >, and & characters using Unicode
escapes to prevent injection through JSON-LD. If you need a different
serialisation strategy (pretty-printing for debugging, custom escaping), swap
the serializer: