Extending the suite

django-seo-suite exposes stable extension points so external packages can add capabilities (custom providers, extra <head> renderers, new schema profiles) without forking or monkey-patching.

This page is a brief map of those points for package authors. End-user projects that only need custom behaviour can usually achieve it through get_seo_metadata overrides, template overrides, or the seo_head_rendering signal rather than building a separate package.


Extension discovery

Any installed Django app that ships a top-level seo_extensions.py module has that module imported automatically at startup (via AppConfig.ready). This is the recommended pattern for registering providers, renderers, or schema profiles:

# mypackage/seo_extensions.py

from seo_suite.extension import (
    Provider,
    SeoMetadata,
    provider_registry,
    PRECEDENCE_PATH,
)


class MyCustomProvider(Provider):
    priority = 35  # between PATH (30) and OBJECT (40)

    def provide(self, context):
        # context has: request, view, object, path, site_id, language
        if context.path and context.path.startswith("/promo/"):
            return SeoMetadata.partial(robots="noindex")
        return None


provider_registry.register(MyCustomProvider())

Alternatively, publish an entry point in the seo_suite.extensions group in your package's pyproject.toml:

[project.entry-points."seo_suite.extensions"]
mypackage = "mypackage.seo_extensions"

Both mechanisms are idempotent and safe to run through Django's autoreload.


Registries

Provider registry. Collects providers queried during each resolution:

from seo_suite.extension import provider_registry, Provider, SeoMetadata

provider_registry.register(my_provider)
provider_registry.unregister("mypackage.providers.MyProvider")

Renderer registry. Collects callables that contribute extra <head> fragments at render time. Each renderer receives (request, finalized_metadata) and returns a string or None:

from seo_suite.extension import renderer_registry

def my_renderer(request, metadata):
    if metadata and metadata.og.get("type") == "article":
        return '<meta name="article:author" content="...">'
    return None

renderer_registry.register("mypackage.my_renderer", my_renderer)

Schema registry. Collects JSON-LD profiles keyed by string:

from seo_suite.schema.registry import schema_registry, SchemaProfile

class MyProfile(SchemaProfile):
    key = "mypkg:CustomType"

    def build(self, obj, context):
        return {"@context": "https://schema.org", "@type": "CustomType", "name": "..."}

schema_registry.register(MyProfile())

Use namespaced keys (e.g. "mypkg:CustomType") to avoid collisions with the free profiles.


Swappable classes

Setting What it controls
SEO_SUITE["RESOLVER_CLASS"] The class responsible for collecting and merging provider layers. Subclass seo_suite.resolver.BaseResolver and override collect_layers, build_canonicalizer, or finalize.
SEO_SUITE["JSONLD_SERIALIZER"] The callable that serialises JSON-LD dicts to strings.

Signals

Signal When What you can do
seo_metadata_resolved After the resolver finalises metadata Mutate metadata in place (only signal where mutation is explicitly supported)
seo_head_rendering On every {% seo_head %} render Return extra <head> HTML strings
seo_cache_invalidate On request to drop a cached entry Send to invalidate a specific object

Import from seo_suite.extension:

from seo_suite.extension import seo_metadata_resolved, seo_head_rendering, seo_cache_invalidate

Abstract models for contrib apps

If your package adds its own DB-backed provider, the abstract base classes are available for reuse:

from seo_suite.contrib.base import AbstractSeoColumns
from seo_suite.contrib.seopath.models import AbstractSeoPath
from seo_suite.contrib.seoobject.models import AbstractSeoObject

SeoColumnsMixin (the column-only mixin, without get_seo_metadata) is also importable from seo_suite.providers.model_mixin if you want the standard column set on a concrete model without inheriting SeoModelMixin.


Precedence constants

Use the public constants when registering providers at specific positions in the precedence ladder:

from seo_suite.extension import (
    PRECEDENCE_GLOBAL,   # 10
    PRECEDENCE_SITE,     # 20
    PRECEDENCE_PATH,     # 30
    PRECEDENCE_OBJECT,   # 40
    PRECEDENCE_VIEW,     # 50
)

Any integer priority is valid. Use values between the public constants (e.g. 35) to slot in between existing layers without the base package having to renumber.