Skip to main content

A lightweight, extensible SEO metadata layer for Django: titles, meta tags, canonical URLs, hreflang, JSON-LD schema and Open Graph for models and views.

Project description

django-seo-suite

PyPI Python Django License Docs

A lightweight, extensible SEO metadata layer for Django. It gives any project one consistent way to attach and render the full set of SEO attributes — <title>, <h1>, meta description/keywords/robots, canonical URL, hreflang alternates, JSON-LD schema and Open Graph / Twitter cards — across models, class-based views (including listing and model-less views), and even third-party models you can't edit.

  • No required apps. The core ships a single migration (the versioned robots.txt table) and works without django.contrib.sites (detected at runtime).
  • One dependency: Django (4.2+). Python 3.10+.
  • Multi-provider resolver. Metadata is merged from several sources by a clear precedence, then rendered by template tags.
  • Built for extension. A separate package can add capability through stable, documented hooks — no forking, no monkeypatching.

Install

pip install django-seo-suite
# settings.py
INSTALLED_APPS = [
    # ...
    "seo_suite",
    # optional, admin-editable overrides:
    "seo_suite.contrib.seopath",     # SEO keyed by URL path
    "seo_suite.contrib.seoobject",   # SEO for third-party models (uses ContentTypes)
]

TEMPLATES = [{
    # ...
    "OPTIONS": {"context_processors": [
        "django.template.context_processors.request",
        "seo_suite.context.seo",     # enables {% seo_head %} everywhere
    ]},
}]

Then run migrations (the core app ships one migration for the robots.txt table):

python manage.py migrate

Quickstart

On a model you own:

from django.db import models
from seo_suite.mixins import SeoModelMixin

class Article(SeoModelMixin, models.Model):
    title = models.CharField(max_length=200)
    summary = models.TextField()
    cover = models.ImageField(upload_to="covers/")
    slug = models.SlugField()

    SEO_FIELD_MAP = {"meta_description": "summary", "og_image": "cover"}
    SEO_SCHEMA_PROFILES = ["Article", "BreadcrumbList"]

    def get_absolute_url(self):
        return f"/articles/{self.slug}/"

title/description/image fall back to conventional field names automatically; SEO_FIELD_MAP overrides them; get_absolute_url becomes the canonical URL.

On a view:

from django.views.generic import DetailView, ListView
from seo_suite.mixins import SeoViewMixin, SeoListViewMixin

class ArticleDetail(SeoViewMixin, DetailView):
    model = Article
    # the object's metadata is used automatically; view attrs override it

class ArticleList(SeoListViewMixin, ListView):     # model-less views work too
    model = Article
    seo_title = "All Articles"                     # becomes "All Articles – Page 2" when paginated

In your base template:

{% load seo_suite %}
<head>
  {% seo_head %}            {# title, meta, canonical, hreflang, OG, Twitter, JSON-LD #}
</head>

Need finer control? Use the granular tags: {% seo_title %}, {% seo_meta %}, {% seo_canonical %}, {% seo_hreflang %}, {% seo_opengraph %}, {% seo_twitter %}, {% seo_jsonld %}, {% seo_extra_head %}. Every partial in templates/seo_suite/ is overridable by shipping your own.

How resolution works

Metadata is merged from lowest to highest precedence (higher wins per field; JSON-LD and extra-head fragments accumulate):

Precedence Source
10 SEO_SUITE["DEFAULTS"] (global)
20 SEO_SUITE["SITE_DEFAULTS"][site_id]
30 seopath.SeoPath row matching the URL path (optional app)
40 the object's get_seo_metadata() (model mixin) or seoobject row
50 the view's get_seo_metadata() / seo_* attributes

A field left "unset" defers to lower precedence; an explicit None/"" wins and renders nothing.

JSON-LD schema library

Enable ready-made schema.org profiles declaratively — no template editing:

class FaqPage(SeoModelMixin, models.Model):
    SEO_SCHEMA_PROFILES = ["WebPage", "FAQPage"]
    def get_faqs(self):
        return [{"question": "…", "answer": "…"}]

Free profiles: WebPage, WebSite, Organization, Person, BreadcrumbList, Article, FAQPage, Product. An object can supply or override fields via get_schema_data(key, context).

Sitemaps & hreflang

from seo_suite.sitemaps import SeoSitemap

class ArticleSitemap(SeoSitemap):
    i18n = True
    alternates = True
    x_default = True
    def items(self):
        return Article.objects.all()

SeoSitemap sets each <loc> to the page's resolved canonical, so the sitemap URL always matches rel="canonical". For on-page alternates, seo_suite.hreflang.build_hreflang_alternates(path, request) produces the same LANGUAGES-driven set the sitemap uses.

Versioned robots.txt

Serve robots.txt from the database with a full version history, so a traffic regression can be traced to a specific change. Add the route to your root urlconf:

# urls.py
urlpatterns = [
    # ...
    path("", include("seo_suite.urls")),   # serves /robots.txt
]

Then manage versions in the Django admin. Adding a version (the form is pre-filled with the current live content) publishes it on save; the version it replaces is kept as a read-only history row. To roll back, run the "Roll back" action on an earlier version. Exactly one version is live per site, enforced at the database level. Every version records created_at, activated_at, and the author, so the admin changelist is a timeline you can line up against your analytics. When no version is active, the view serves SEO_SUITE["ROBOTS_TXT_FALLBACK"]; SEO_SUITE["ROBOTS_SITEMAP_URLS"] are appended as Sitemap: lines.

Settings (SEO_SUITE)

Key Default Purpose
RESOLVER_CLASS seo_suite.resolver.Resolver swap the resolver
SITE_ID_RESOLVER None custom current-site callable
DEFAULTS {robots, og.type, ...} global metadata seed
SITE_DEFAULTS {} per-site metadata seed
CANONICAL_DOMAIN / FORCE_HTTPS_CANONICAL None / True canonical absolutization
LIST_CANONICAL_INCLUDES_PAGE True paginated-list canonical policy
HREFLANG_X_DEFAULT True emit x-default
CACHE_TTL 0 cache resolved payload (0 = off)
OBJECT_MODELS [] allowlist "app.model" for seoobject
DEFAULT_SCHEMA_PROFILES [] profiles applied site-wide
ROBOTS_TXT_FALLBACK "User-agent: *\nAllow: /" served when no robots.txt version is active
ROBOTS_SITEMAP_URLS [] URLs appended to robots.txt as Sitemap: lines
ROBOTS_CACHE_TTL 0 cache served robots.txt (0 = off)

Extending the suite (public contract)

These are versioned, stable APIs an extension package may rely on:

  • Registriesprovider_registry.register(provider, priority=…), renderer_registry.register(name, callable).
  • Swappable classesSEO_SUITE["RESOLVER_CLASS"], ["JSONLD_SERIALIZER"].
  • Signalsseo_metadata_resolved (mutation allowed), seo_cache_invalidate, seo_head_rendering.
  • Schema profilesschema_registry.register(profile) with namespaced keys.
  • Abstract modelsAbstractSeoPath, AbstractSeoObject, SeoColumnsMixin.
  • Autodiscovery — ship a top-level seo_extensions.py (imported at startup) or publish an entry point in the seo_suite.extensions group.
  • Templates — override any templates/seo_suite/* partial.

Everything imports from one surface:

# my_extension/seo_extensions.py
from seo_suite.extension import PRECEDENCE_PATH, Provider, SeoMetadata, provider_registry

class MyProvider(Provider):
    priority = 35
    def provide(self, context):
        return SeoMetadata.partial(meta_description="…")

provider_registry.register(MyProvider())

Development

pip install -e ".[dev]"
pytest                                   # contrib apps on, sites off
SEO_SUITE_TEST_SITES=1 pytest            # sites framework on
SEO_SUITE_TEST_NO_CONTRIB=1 pytest       # core only
SEO_SUITE_TEST_PRO=1 pytest              # with a fake extension package
ruff check seo_suite tests
tox                                      # full Python × Django matrix

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_seo_suite-0.1.1.tar.gz (50.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_seo_suite-0.1.1-py3-none-any.whl (51.1 kB view details)

Uploaded Python 3

File details

Details for the file django_seo_suite-0.1.1.tar.gz.

File metadata

  • Download URL: django_seo_suite-0.1.1.tar.gz
  • Upload date:
  • Size: 50.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_seo_suite-0.1.1.tar.gz
Algorithm Hash digest
SHA256 c66fb1c19345ad2681d2eaef99bf98ed413fbca887650928cd1498187b63657a
MD5 c1884037bc3c7d4c16d787acafd3aa21
BLAKE2b-256 5bd567bda31412be2c9fd86ddb21a7b71498c8587d3c171d90cb8c17c7c65b5f

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_seo_suite-0.1.1.tar.gz:

Publisher: publish.yml on specivo/django-seo-suite

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_seo_suite-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_seo_suite-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d2c74ddd75883952b750bec1647f9b0dbcfec2f816e42a6b5fa0017bf3483141
MD5 76d28128b3f6cf0e6768787e9863d9f3
BLAKE2b-256 e34355dcf6720adbba76530a4f2f57a7d58d2691d07f57db3518a84784f73187

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_seo_suite-0.1.1-py3-none-any.whl:

Publisher: publish.yml on specivo/django-seo-suite

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page