Structured data is how Google and other search engines provide rich search results like ratings, cooking times, or article publish dates. Google supports three structured data formats: JSON-LD, Microdata, and RDFa, with Google recommending JSON-LD. This article demonstrates how to implement a flexible JSON-LD system in Django to improve site search visibility.
JSON-LD Implementation Difficulties
Adding structured data to a Django site often surfaces a subtle challenge: the need to inject JSON-LD in multiple places throughout the codebase. Ideally, structured data should be:
- Defined site-wide (e.g. for
WebSite
orOrganization
schemas) - Customized per view or model (e.g.
BlogPosting
orProduct
) - Extended dynamically for specific pages (e.g. conditional FAQ data, event metadata, etc.)
Django doesn't offer a clean way to combine these layers into a single JSON-LD output. Instead, attempts often lead to:
- JSON's strict syntax, where a trailing comma could break the script
- Multiple
<script>
tags, cluttering the HTML head - Duplicated logic across templates and views
- No clear separation between site-wide data and page-specific schemas
The Solution
This implementation solves the problem by combining three concepts:
- The @graph property in JSON-LD allows multiple schemas to be combined into a single JSON-LD object. This ensures all structured data can be output as a single
<script>
tag while preserving semantic relationships. - A context processor defines global structured data (e.g. the
WebSite
schema) and initializes the structure needed for downstream use. - Template tags and the view context allow each view to append page-specific data to an object named
structured_data_list
. These entries are merged automatically in the final rendered output.
Example output:
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "WebSite", "name": "YourSite", ... },
{ "@type": "BlogPosting", "headline": "Your Post", ... },
{ "@type": "Person", "name": "Author Name", ... }
]
}
A single script tag keeps structured data manageable while enhancing semantic relationships for search engines.
Implementation
Context Processor
The context processor defines default structured data shared across all pages on the website (top-level objects like WebSite
), and initializes an empty list for view-specific data to be appended later.
# core/context_processors.py
def jsonld_context(request):
"""Add site-wide JSON-LD data to context."""
website_jsonld = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "YourSite.com",
"url": request.build_absolute_uri('/'),
}
return {
'website_jsonld': website_jsonld,
'structured_data_list': []
}
Register the processor in TEMPLATES
within settings.py
:
TEMPLATES = [
{
'OPTIONS': {
'context_processors': [
# ... other processors
'core.context_processors.jsonld_context',
],
},
},
]
Custom Template Tags
The template tag handles final rendering. It collects both site-wide and view-specific schema objects, combines them into an @graph
, and outputs a single JSON-LD script tag. This ensures clean, valid markup without duplication.
# core/templatetags/jsonld_tags.py
import json
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.simple_tag(takes_context=True)
def render_all_jsonld(context):
"""Render all structured data items as a JSON-LD graph."""
items = context.get('structured_data_list', []).copy()
# Add website data if it exists
if 'website_jsonld' in context:
website_data = context['website_jsonld'].copy()
if "@context" in website_data:
del website_data["@context"]
items.append(website_data)
if not items:
return ''
# Create the graph
graph = {
"@context": "https://schema.org",
"@graph": items
}
# Render as script tag
json_str = json.dumps(graph, indent=2)
return mark_safe(f'<script type="application/ld+json">{json_str}</script>')
Base Template Integration
Include the rendering tag in the base template’s <head>
block. This ensures the combined JSON-LD output appears on every page that uses the layout.
{% load jsonld_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Other head elements -->
{% block json_ld %}
{% render_all_jsonld %}
{% endblock json_ld %}
</head>
<body>
<!-- Page content -->
</body>
</html>
Implementing Schemas in Views
Individual views can contribute structured data to the page by adding items to structured_data_list
. This allows each page to describe its specific content while still benefiting from the shared base setup.
Blog Post List View
This view defines two schema entries:
- A
CollectionPage
to describe the list view as a whole - An
ItemList
to expose the individual blog posts with headlines and publish dates
def post_list(request):
posts = Post.objects.filter(is_published=True).order_by('-created_at')
# Define the collection page schema
blog_list_data = {
"@type": "CollectionPage",
"name": "Blog Posts",
"description": "Collection of blog posts"
}
# Add an ItemList for better search result display
if posts.exists():
articles_data = {
"@type": "ItemList",
"itemListElement": [
{
"@type": "ListItem",
"position": i+1,
"item": {
"@type": "BlogPosting",
"headline": post.title,
"url": request.build_absolute_uri(post.get_absolute_url()),
"datePublished": post.created_at.isoformat()
}
} for i, post in enumerate(posts[:10]) # Limit for performance
]
}
structured_data_list = [blog_list_data, articles_data]
else:
structured_data_list = [blog_list_data]
return render(request, 'blog/post_list.html', {
'posts': posts,
'structured_data_list': structured_data_list
})
Blog Post Detail View
This view describes a single blog post using the BlogPosting
schema. If an image is available, it's included as well.
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug)
# Create detailed blog post schema
blog_data = {
"@type": "BlogPosting",
"headline": post.title,
"description": post.description,
"author": {
"@type": "Person",
"name": "Your Name",
},
"datePublished": post.created_at.isoformat(),
"dateModified": post.updated_at.isoformat(),
"mainEntityOfPage": {
"@type": "WebPage",
"@id": request.build_absolute_uri(post.get_absolute_url())
}
}
# Add image if available
if post.featured_image:
blog_data["image"] = request.build_absolute_uri(post.featured_image.url)
return render(request, 'blog/post_detail.html', {
'post': post,
'structured_data_list': [blog_data]
})
Extending the System
The structured data can be expanded further by adding to the structured_data_list
.
This view demonstrates a Product
schema with nested Offer
data. The availability dynamically reflects inventory status.
def product_detail(request, slug):
product = get_object_or_404(Product, slug=slug)
product_data = {
"@type": "Product",
"name": product.name,
"description": product.description,
"offers": {
"@type": "Offer",
"price": str(product.price),
"priceCurrency": "USD",
"availability": "https://schema.org/InStock" if product.in_stock else "https://schema.org/OutOfStock"
}
}
return render(request, 'shop/product_detail.html', {
'product': product,
'structured_data_list': [product_data]
})
Takeaways
This approach to structured data in Django separates concerns while keeping output clean and valid. Key advantages include:
- Single script tag output: combines all structured data into one
<script>
using the@graph
property, avoiding clutter and validation issues. - Clear separation of concerns: site-wide schemas live in the context processor, while view-specific schemas are defined close to their data
- Composable architecture: any view can append data to
structured_data_list
, allowing incremental adoption and page-level customization.