How to Create Custom Titles for Each Grav CMS Page
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.
Table of Contents
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 namedtitle_content
. -
page.route == '/' ? site.title :
- If thepage.route
is/
(Homepage) then get thesite.title
from thesite.yaml
file and assign it to the variabletitle_content
. -
page.header.custom_title ? page.header.custom_title :
- If the page has acustom_title
assigned in the frontmatter, get thecustom_title
and assign it to the variabletitle_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 variabletitle_content
. -
'Eric Stauffer'
- This is the fallback title assigned totitle_content
if none of the above conditions are met. -
<title>{{ title_content }}</title>
- Insertstitle_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.
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:
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!