Plain is headed towards 1.0! Subscribe for development updates →

Dynamic page titles in Plain and Django

Guide by @davegaeddert • 2023

Using class-based views or template blocks to dynamically generate page titles in a Plain or Django app.

It's easy to forget about the HTML <title> element when building a Plain or Django app. You put one in your base.html when you start the project, but forget to actually change the title when making your individual views and templates.

<!doctype html>
<html lang="en">
<head>
    <title>Forge</title>
</head>

But good page titles are incredibly useful!

Forgetting them can hurt both SEO and user experience.

 

There are two ways we recommend implementing HTML page titles in Plain or Django:

  1. Using template blocks (simpler)
  2. Using class-based views (more powerful)

Page titles using template blocks

Template blocks can be used for small pieces of content just like they can be used for entire headers/bodies/footers.

In your base.html, use a standard template block inside the <title> tag:

<!doctype html>
<html lang="en">
<head>
    <title>{% block title %}{% endblock %}</title>
</head>

When you extend your template, simply use the {% block title %} to set the page title:

{% extends "base.html" %}

{% block title %}Billing{% endblock %}

{% block content %}
...
{% endblock %}

You can use context variables just like any other template blocks:

{% extends "base.html" %}

{% block title %}{{ obj.name }} billing{% endblock %}

{% block content %}
...
{% endblock %}

That's all there is to it! If you don't know where to start then give this a try. But if you find yourself needing more control then consider moving the logic to your views instead...

Page titles using class-based views

Let's create a view mixin that injects a title into the template context:

# (Plain)
class PageTitleViewMixin:
    title = ""

    def get_title(self):
        """
        Return the class title attr by default,
        but you can override this method to further customize
        """
        return self.title

    def get_template_context(self):
        context = super().get_template_context()
        context["title"] = self.get_title()
        return context
# (Django)
class PageTitleViewMixin:
    title = ""

    def get_title(self):
        """
        Return the class title attr by default,
        but you can override this method to further customize
        """
        return self.title

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["title"] = self.get_title()
        return context

Then use {{ title }} in your base.html:

<!doctype html>
<html lang="en">
<head>
    <title>{{ title }}</title>
</head>

By using both a title class attribute and a get_title method, it will be easy to set a "static" title for a view:

class BillingView(PageTitleViewMixin, TemplateView):
    template_name = "billing.html"
    title = "Billing"

But also do something more dynamic:

class ProjectView(PageTitleViewMixin, DetailView):
    def get_title(self):
        return self.object.name

You could further extend this to add a suffix to the title automatically:

# (Plain)
class PageTitleViewMixin:
    ...

    def get_template_context(self):
        context = super().get_template_context()
        context["title"] = self.get_title() + " - My App"
        return context
# (Django)
class PageTitleViewMixin:
    ...

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["title"] = self.get_title() + " - My App"
        return context

Bonus! You could also raise an exception on empty titles, so you don't forget to set the title for every view:

# (Plain)
class PageTitleViewMixin:
    ...

    def get_template_context(self):
        context = super().get_template_context()

        title = self.get_title()

        if not title:
            raise ValueError("Page title should not be empty")

        context["title"] = title + " - My App"

        return context
# (Django)
class PageTitleViewMixin:
    ...

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        title = self.get_title()

        if not title:
            raise ValueError("Page title should not be empty")

        context["title"] = title + " - My App"

        return context