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