How to Create Custom Titles for Each Grav CMS Page

frontmatter-expert

If you're looking to create custom header titles for specific pages on your Grav CMS website, you found the right place.

There are a number of reasons why you might want more control over the title in the header of your website pages. Search results and SEO are obvious reasons, but I'll dive into my specific use case and how I solved it.

Base Template

The theme I am using builds the header title by default through the base.html.twig template. You can find it in /user/themes/THEME-NAME/templates/partials/. Here is what the header section looked like in mine (your's may look slightly different):

{% block head %}
    <meta charset="utf-8" />
    <title>{% if header.title %}{{ header.title }} | {% endif %}{{ site.title }}</title>
    {% include 'partials/metadata.html.twig' %}
    ...
{% endblock head %}    

You can see between the <title></title> tags is the logic that builds the page titles:

<title>{% if header.title %}{{ header.title }} | {% endif %}{{ site.title }}</title>

The default setup in my template pulls the title of the page using {% if header.title %}{{ header.title }}, adds a pipe |, then grabs the site title from the site.yaml file. If you are using the Admin UI plugin, you can set the site title directly on the configuration page after logging in.

This default configuration gets you moving, but it has limitations.

In my case, I set the site title to Eric Stauffer so it would append | Eric Stauffer to the end of each blog post. However, I started running into issues when I was naming ones like the default Blog page and the site's homepage.

I wanted the homepage title to show in search results like:

Eric Stauffer | Web Dev Expert in Grav CMS & WordPress

If I tried to use this as the page title with the default setup, it came out like this:

Eric Stauffer | Web Dev Expert in Grav CMS & WordPress | Eric Stauffer

Additionally, that would have shown up on the homepage as an H1 tag, unless I hid it with CSS or disabled it some other way. Both of those routes seemed messy.

Set Function & Conditional (Ternary) Operator

As with most things in development, there are multiple ways to solve this problem. I wanted a clean and scalable solution that would future-proof any potential need for many custom titles down the road.

1. Utilize metadata.html.twig

Moving the header title logic off of the base.html.twig template is the first step. If you haven't done so already, copy /system/template/partials/metadata.html.twig to /user/themes/templates/partials/metadata.html.twig.

Mine looked like this to start:

{% for meta in page.metadata %}
    <meta {% if meta.name %}name="{{ meta.name|e }}" {% endif %}{% if meta.http_equiv %}http-equiv="{{ meta.http_equiv|e }}" {% endif %}{% if meta.charset %}charset="{{ meta.charset|e }}" {% endif %}{% if meta.property %}property="{{ meta.property|e }}" {% endif %}{% if meta.content %}content="{{ meta.content|raw }}" {% endif %}/>
{% endfor %}

Now we are going to insert the set function at the top. The basic structure of it looks like this:

{% set variable_name = 
    condition1 ? value1 :
    condition2 ? value2 :
    condition3 ? value3 :
    ... :
    fallback_value 
%}

After filling these in with custom conditions, mine looked like this:

{% set title_content = 
    page.route == '/' ? site.title :
    page.header.custom_title ? page.header.custom_title :
    header.title ? header.title ~ ' | Eric Stauffer' :
    'Eric Stauffer'
%}
<title>{{ title_content }}</title>

Lets examine what these do:

  • {% set title_content = - Creates a variable named title_content.

  • page.route == '/' ? site.title : - If the page.route is / (Homepage) then get the site.title from the site.yaml file and assign it to the variable title_content.

  • page.header.custom_title ? page.header.custom_title : - If the page has a custom_title assigned in the frontmatter, get the custom_title and assign it to the variable title_content.

  • header.title ? header.title ~ ' | Eric Stauffer' : - If the page has a standard title, append | Eric Stauffer to then end and assign it to the variable title_content.

  • 'Eric Stauffer' - This is the fallback title assigned to title_content if none of the above conditions are met.

  • <title>{{ title_content }}</title> - Inserts title_content into the header title tags.

Whenever a page is called, this set function starts at the top and works its way down. Once one of the conditions are met, it assigns the variable and builds the title.

Here is what my metadata.html.twig looked like when it was done:

{% set title_content = 
    page.route == '/' ? site.title :
    page.header.custom_title ? page.header.custom_title :
    header.title ? header.title ~ ' | Eric Stauffer' :
    'Eric Stauffer'
%}
<title>{{ title_content }}</title>

{% for meta in page.metadata %}
    <meta {% if meta.name %}name="{{ meta.name|e }}" {% endif %}{% if meta.http_equiv %}http-equiv="{{ meta.http_equiv|e }}" {% endif %}{% if meta.charset %}charset="{{ meta.charset|e }}" {% endif %}{% if meta.property %}property="{{ meta.property|e }}" {% endif %}{% if meta.content %}content="{{ meta.content|raw }}" {% endif %}/>
{% endfor %}

Update Frontmatter

The first and third conditions in the set function above use default items found in almost every Grav install. The second condition uses custom_title. This is a custom entry that should be added to the frontmatter of any page you want to customize.

You can do this on the page markdown file directly, or through the expert settings in the Admin plugin UI.

frontmatter

frontmatter-expert

Edit base.html.twig

Depending on the theme installed, you may need to update the base template and/or any other templates that create a title tag. In my case, base.html.twig was where the title logic was housed.

If you followed the instructions above and updated the metadata.html.twig file, you should remove the code that creates the title tag in the base template. It will look something like this:

<title>{% if header.title %}{{ header.title }} | {% endif %}{{ site.title }}</title>

Now you need the base.html.twig template to include the updated metadata.html.twig file when building the header. Make sure the base template has the following line in the block head:

{% include 'partials/metadata.html.twig' %}

Like this:

{% block head %}
    <meta charset="utf-8" />
    {% include 'partials/metadata.html.twig' %}
    ...

Test Changes

After saving all the files, make sure to clear the site's cache before testing. If you are not seeing the changes you expect, clear the cache again and try it in a private browser. Grav is notoriously aggressive with caching, and this is often the reason you don't immediately see the updates.

A quick way to check if your custom_title is working is by looking at the page source code. You should see the new title near the top of the <head> section:

custom-header-title

Final Thoughts

If you use my example as a template, remember that the first condition looks for the homepage route and builds the title based on my example. Even if you put a custom_title in the homepage frontmatter, it will stop once the first condition is met.

To control the homepage title with custom_title, remove this first condition from the set function:

page.route == '/' ? site.title :

Good luck!

More Grav Guides