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.
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: