Grav Page Meta & Frontmatter — Advanced YAML and Image Headers
In this tutorial, you'll learn advanced Grav page meta and frontmatter techniques including YAML overrides, image headers, custom metadata fields, and SEO optimization strategies.
What You'll Learn
- Advanced YAML frontmatter: nested fields, arrays, multisite overrides
- Image headers: setting page images, social share images, thumbnails
- Custom metadata fields and how to use them in Twig
- Language-specific frontmatter overrides
- SEO metadata: Open Graph, Twitter Cards, schema markup
- Frontmatter inheritance and merging rules
Why It Matters
In WordPress, page metadata is handled by plugins like Yoast SEO. In Grav, metadata lives directly in the page's YAML frontmatter — no plugins required for basic SEO. Every field you add to frontmatter becomes available in your Twig templates. This means you can create custom fields for anything: page icons, background colors, sidebar layouts, author bios. Understanding advanced frontmatter gives you total control over page presentation without touching PHP.
Real-World Use
A documentation site with 500 pages needs different Open Graph images for each product section. Rather than using a plugin, each page defines its own og_image in frontmatter. The base template reads this field and generates proper social share tags. When the marketing team adds a new product, they simply add the og_image field to the frontmatter — no code changes needed.
Learning Path
flowchart LR
A["Custom Page Types"] --> B["Page Meta & Frontmatter
← You are here"]:::current
B --> C["Page Collections"]
C --> D["Markdown & Shortcodes"]
D --> E["Twig Filters & Functions"]
E --> F["Twig Macros"]
F --> G["Twig Inheritance"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Frontmatter Basics Review
Every Grav page starts with YAML frontmatter between --- delimiters:
---
title: My Page
published: true
menu: My Page
---
Grav recognizes standard fields like title, published, menu, body_classes, template, visible, and route. But you can add any custom field you want.
Advanced YAML Techniques
Nested Fields
Frontmatter supports nested YAML structures:
---
title: About Us
author:
name: Jane Doe
email: jane@example.com
role: Technical Writer
social:
twitter: "@janedoe"
github: janedoe
page_settings:
sidebar: true
sidebar_width: 300
show_toc: true
---
Access these in Twig:
<p>Written by {{ page.header.author.name }}</p>
{% if page.header.page_settings.sidebar %}
<aside style="width: {{ page.header.page_settings.sidebar_width }}px">
{{ page.content|raw }}
</aside>
{% endif %}
Arrays and Lists
---
title: Documentation
tags:
- grav
- cms
- tutorial
contributors:
- name: Alice
role: Editor
- name: Bob
role: Reviewer
---
Iterate in Twig:
<ul>
{% for tag in page.header.tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
Multisite Overrides
Grav supports environment-aware frontmatter using the @ suffix:
---
title: My Site
title@dev: My Dev Site
title@prod: My Production Site
published: true
published@dev: false
---
On your development environment, the page title is "My Dev Site" and published is false. On production, the title is "My Production Site" and published is true. This prevents staging pages from appearing in search results.
Language-Specific Overrides
---
title: Home
title@es: Inicio
title@fr: Accueil
menu: Home
menu@es: Inicio
menu@fr: Accueil
---
The language prefix @es applies only when the site is viewed in Spanish. Grav falls back to the base value when no override exists.
Image Headers
Grav treats certain frontmatter fields as image references:
Page Image
---
title: Blog Post
image:
src: /images/blog-hero.jpg
alt: Blog post hero image
width: 1200
height: 630
---
Open Graph Image (Social Sharing)
---
title: Blog Post
og_image: /images/og-default.jpg
twitter_image: /images/twitter-card.jpg
---
Access in Twig:
<meta property="og:image" content="{{ base_url_absolute ~ page.header.og_image }}" />
<meta name="twitter:image" content="{{ base_url_absolute ~ page.header.twitter_image }}" />
Page Thumbnail (for listings)
---
title: Blog Post
thumbnail: /images/thumbs/post-thumb.jpg
---
{% if page.header.thumbnail %}
<img src="{{ page.media[page.header.thumbnail].url }}" alt="{{ page.title }}" />
{% endif %}
SEO Metadata
Standard Meta Tags
---
title: Tutorial Page
metadata:
description: A complete guide to Grav frontmatter.
keywords: grav, frontmatter, yaml, seo
author: DodaTech
robots: index, follow
---
The base template renders these automatically if it includes:
{% for key, value in page.metadata %}
<meta name="{{ key }}" content="{{ value }}" />
{% endfor %}
Open Graph Tags
---
title: Tutorial Page
og:
title: Grav Frontmatter Complete Guide
description: Learn every aspect of Grav YAML frontmatter.
type: article
url: "{{ page.url(true) }}"
image: /images/og-grav.jpg
---
<meta property="og:title" content="{{ page.header.og.title ?: page.title }}" />
<meta property="og:description" content="{{ page.header.og.description ?: page.metadata.description }}" />
<meta property="og:type" content="{{ page.header.og.type ?: 'website' }}" />
<meta property="og:url" content="{{ page.url(true) }}" />
<meta property="og:image" content="{{ base_url_absolute ~ page.header.og.image }}" />
Twitter Cards
---
title: Tutorial Page
twitter:
card: summary_large_image
site: "@dodatech"
creator: "@dodatech"
---
<meta name="twitter:card" content="{{ page.header.twitter.card ?: 'summary' }}" />
<meta name="twitter:site" content="{{ page.header.twitter.site }}" />
<meta name="twitter:creator" content="{{ page.header.twitter.creator }}" />
Frontmatter Inheritance
When a child page is inside a folder with a folder.md, frontmatter fields are inherited:
Parent (folder.md):
---
title: Documentation
body_classes: docs-section
metadata:
description: Documentation section
layout: sidebar
---
Child (default.md):
---
title: Getting Started
layout: full-width # Overrides parent layout
---
The child's layout: full-width overrides the parent's layout: sidebar. Fields not specified in the child (body_classes, metadata) inherit from the parent.
Merge behavior:
| Field Type | Behavior |
|---|---|
| Scalar (string, number) | Child overrides parent |
| List (array) | Child replaces parent entirely |
| Hash (key-value) | Child merges with parent (child keys win) |
Conditional Frontmatter
Use Twig logic in templates based on frontmatter values:
---
title: Landing Page
show_sidebar: false
show_footer: true
custom_css: /css/landing.css
---
{% if not page.header.show_sidebar %}
<main class="full-width">
{% else %}
<main class="with-sidebar">
{% endif %}
{{ page.content|raw }}
</main>
{% if page.header.custom_css %}
<link rel="stylesheet" href="{{ url(page.header.custom_css) }}" />
{% endif %}
Frontmatter in Collection Items
When listing child pages, frontmatter fields are available per item:
{% for child in page.children %}
<article>
<h2>{{ child.title }}</h2>
<p class="date">{{ child.header.date|date('F j, Y') }}</p>
{% if child.header.thumbnail %}
<img src="{{ child.media[child.header.thumbnail].url }}" alt="" />
{% endif %}
<p>{{ child.summary }}</p>
</article>
{% endfor %}
Common Mistakes
Invalid YAML indentation: YAML is indentation-sensitive. Using spaces inconsistently (2 spaces vs 4 spaces) will cause parse errors. Always use consistent indentation — 2 spaces is the Grav convention.
Forgetting the closing
---: Every frontmatter block must end with---. Missing it causes Grav to treat the entire page as frontmatter.Using tabs instead of spaces: YAML does not support tabs. Convert all tabs to spaces. Most editors have a "convert indentation to spaces" option.
Overwriting parent lists instead of merging: When a child page has a list field (like
tags:), the child's list completely replaces the parent's. You cannot partially inherit list items.Not escaping special YAML characters: Values containing colons,
#, or brackets must be quoted:title: "My Page: A Guide". Unquoted colons confuse the YAML parser.
Practice Questions
How do you create a language-specific frontmatter override for Spanish? Answer: Add the
@essuffix to the field name:title@es: Inicio. Grav applies this when the site language is set to Spanish.What happens when a child page defines a field that also exists in the parent
folder.md? Answer: The child's value overrides the parent's value for scalar fields. Hashes merge with child keys winning. Lists are completely replaced.How do you add Open Graph metadata to a page? Answer: Add an
og:section in frontmatter with fields liketitle,description,type,url, andimage. Then render them in your Twig template.What is the purpose of
@devand@prodfrontmatter suffixes? Answer: They allow environment-specific overrides. For example,published@dev: falseprevents a page from appearing on the development site while it is published on production.Challenge: Create a complete SEO metadata configuration for a blog post. Include: standard meta description and keywords, Open Graph tags with custom title and image, Twitter Card tags with
summary_large_imagetype, a customreading_timefield calculated from word count, anauthorhash with name, bio, and avatar URL, and aseriesfield that groups related posts. Then write a Twig template that renders all metadata tags in the<head>section and displays the author box at the bottom of the post.
FAQ
{{< faq "Can I use frontmatter to set per-page CSS or JS?" "Yes. Add a custom_css or custom_js field with the file path. In your Twig template, conditionally include the asset: <link rel=\"stylesheet\" href=\"{{ url(page.header.custom_css) }}\" />." >}}
{{< faq "How do I prevent a page from being indexed by search engines?" "Add metadata.robots: noindex, nofollow in frontmatter. Your base template must render the meta robots tag: <meta name=\"robots\" content=\"{{ page.header.metadata.robots }}\" />." >}}
Mini Project
Goal: Create a blog post template that uses advanced frontmatter for rich presentation.
- Create a blog page folder with
item.md - Add frontmatter with: title, date, author (name, bio, avatar), metadata (description, keywords, robots), og_image, twitter_card, reading_time, tags (list), series, custom_css
- Create a Twig template that renders:
- SEO meta tags in the head (Open Graph, Twitter Cards, standard meta)
- Article header with title, author, date, reading time
- Author bio box with avatar at the bottom
- Tag links as a footer section
- Series navigation (prev/next within the same series)
- Create a second page with different frontmatter values
- Verify the Open Graph tags render correctly using a social media debugger
- Test language override by adding
title@esand switching site language
What's Next
Now you control every aspect of page metadata. Next, learn how to collect and display groups of pages:
Continue to Lesson 13: Page Collections — List pages, filter children, traverse siblings, and build modular collections.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro