Guide: Model-less views

SeoViewMixin works for any class-based view, including TemplateView and custom views that have no associated model object. Use class attributes for static pages or override get_seo_metadata for dynamic ones.


Static pages with class attributes

# pages/views.py

from django.views.generic import TemplateView
from seo_suite.mixins import SeoViewMixin


class AboutView(SeoViewMixin, TemplateView):
    template_name = "pages/about.html"
    seo_title = "About Us"
    seo_description = "Learn who we are and what we do."
    seo_canonical = "/about/"


class PrivacyPolicyView(SeoViewMixin, TemplateView):
    template_name = "pages/privacy.html"
    seo_title = "Privacy Policy"
    seo_robots = "noindex"

All seo_* class attributes are picked up automatically at priority 50 (the view layer). Global defaults from SEO_SUITE["DEFAULTS"] still apply below them, so robots, title_suffix, and og:type are inherited without repetition.


Dynamic titles from the request or URL kwargs

Override get_seo_title for titles that depend on runtime values:

class SearchResultsView(SeoViewMixin, TemplateView):
    template_name = "search/results.html"

    def get_seo_title(self, context=None):
        query = self.request.GET.get("q", "")
        if query:
            return f'Search results for "{query}"'
        return "Search"

    def get_seo_canonical(self, context=None):
        # Canonical strips the query string; all searches share one canonical.
        return "/search/"

Fully dynamic metadata with get_seo_metadata

For complete control, override get_seo_metadata and return a SeoMetadata.partial(...):

from seo_suite.mixins import SeoViewMixin
from seo_suite.metadata import SeoMetadata
from django.views.generic import TemplateView


class UserDashboardView(SeoViewMixin, TemplateView):
    template_name = "dashboard/index.html"

    def get_seo_metadata(self, context=None):
        user = self.request.user
        return SeoMetadata.partial(
            title=f"{user.first_name}'s Dashboard",
            robots="noindex,nofollow",  # personal pages should not be indexed
        )

SeoMetadata.partial(**kwargs) only sets the fields you name; everything else remains UNSET so global defaults fill the gaps.


Function-based views

For function-based views, use resolve_seo directly and pass the result into the template context:

from seo_suite.context import resolve_seo
from django.shortcuts import render


def contact_view(request):
    seo = resolve_seo(request)
    # Without a view or object, the context processor's lazy resolution is used
    # automatically. You can also construct a SeoMetadata and attach it manually
    # by passing a lightweight wrapper.
    return render(request, "pages/contact.html", {"seo": seo})

Alternatively, rely on the seo context processor entirely. Because the context processor is lazy, the seo variable is available in every template that uses {% seo_head %}, including those rendered by function-based views. You just cannot inject view-layer overrides without using the mixin.


Accessing h1 in the template

The resolved h1 field is available in any template as {{ seo.h1 }}. When it is set:

<h1>{% if seo.h1 %}{{ seo.h1 }}{% else %}{{ seo.title }}{% endif %}</h1>

Or, since seo.h1 is None when unset, use the default filter:

<h1>{{ seo.h1|default:seo.title }}</h1>