Guide: Third-party models (seoobject)

When the model you need to add SEO to belongs to a third-party package (for example a Product model from django-oscar, a Post from a CMS package, or any other model where you cannot add a mixin), the seoobject contrib app lets editors manage SEO through a separate linked table using Django's generic relations.


When to use seoobject

Use seoobject when:

  • The model is in a third-party package you cannot modify.
  • You want admin-editable per-object SEO without patching the model.
  • You want the same admin-editable approach but your own model already has SeoModelFieldsMixin. In that case use the inline columns directly instead.

For models you own, prefer SeoModelMixin (automatic, zero columns) or SeoModelFieldsMixin (editor-editable columns). seoobject adds a ContentTypes query to every resolution of an allowlisted object.


Installation

Add both apps to INSTALLED_APPS:

INSTALLED_APPS = [
    "django.contrib.contenttypes",  # usually already present
    "seo_suite",
    "seo_suite.contrib.seoobject",
]

Run migrations:

python manage.py migrate

Allowlist the model

The seoobject provider only acts on models you explicitly allowlist. This prevents a ContentTypes query on every object resolution; unlisted models are skipped with no database hit.

# settings.py

SEO_SUITE = {
    "OBJECT_MODELS": [
        "catalogue.product",   # app_label.model_name, lowercase
        "blog.post",
    ],
}

The format is "app_label.model_name" (case-insensitive).


Managing SEO in Django Admin

Once the app is installed and a model is allowlisted, editors can create and edit SeoObject rows from the Django Admin at SEO Suite — Object rules → SEO object rules.

Each row stores:

Field Purpose
Content type Which model this row applies to
Object ID The primary key of the specific object
Site ID Optional: restrict to a specific site
Language Optional: restrict to a specific language
Title Overrides the page title
Description Overrides the meta description
Keywords Overrides meta keywords
Robots Overrides the robots directive
Canonical Overrides the canonical URL
OG image Overrides the social sharing image
Extra JSON-LD Additional JSON-LD objects (accumulated, not replaced)

Leave any field blank to defer to lower-priority defaults.


How resolution works

When the resolver processes a request for an allowlisted object, it queries the SeoObject table for rows matching the object's content type and primary key. The specificity rules are:

  1. A row with a matching site_id and language wins.
  2. A row with a matching site_id but no language is next.
  3. A row with no site_id and no language (blank both) applies to all sites and languages.

The winning row is merged as the object layer (priority 40). An object that also defines get_seo_metadata() (i.e. has SeoModelMixin) has its output merged at the same layer. The SeoObject row takes precedence because it is applied after the mixin call.

Note

If the object is not in OBJECT_MODELS, no SeoObject lookup is performed. The model's own get_seo_metadata() (if any) is still called as normal.


Example: django-oscar Product

# settings.py
SEO_SUITE = {
    "OBJECT_MODELS": ["catalogue.product"],
}
# yourapp/views.py
from django.views.generic import DetailView
from seo_suite.mixins import SeoViewMixin
from oscar.apps.catalogue.models import Product


class ProductDetailView(SeoViewMixin, DetailView):
    model = Product
    template_name = "catalogue/product_detail.html"

The view passes the Product instance as the context object. The resolver checks the allowlist, finds catalogue.product, queries SeoObject, and merges any matching row. If no row exists, resolution falls back to global defaults.