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.0.tar.gz (50.1 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.0-py3-none-any.whl (51.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_seo_suite-0.1.0.tar.gz
  • Upload date:
  • Size: 50.1 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.0.tar.gz
Algorithm Hash digest
SHA256 906c2d28fe51093a7b556cf5103534da7bca321befc37a399e38cecd7b80a250
MD5 dc304f2e4a870cbacfa5f160e3ef7a39
BLAKE2b-256 b237e0991602f7ec54dfb90456cee52f5dbbcf746014baf09f617e1687171dcb

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_seo_suite-0.1.0.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.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_seo_suite-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f2d6a63d7fd27145d079fb3d46d51113bedeca2ae2564e74909581346c7ae63a
MD5 6c6ff2ef027b9d184fefd12089bad514
BLAKE2b-256 a9a1aa9d4beadd29b05f79281353574bade759906a92b33d4a156b35b596f01f

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_seo_suite-0.1.0-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