Skip to content

Grav Page Meta & Frontmatter — Advanced YAML and Image Headers

DodaTech Updated 2026-06-27 8 min read

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

  1. 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.

  2. Forgetting the closing ---: Every frontmatter block must end with ---. Missing it causes Grav to treat the entire page as frontmatter.

  3. Using tabs instead of spaces: YAML does not support tabs. Convert all tabs to spaces. Most editors have a "convert indentation to spaces" option.

  4. 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.

  5. 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

  1. How do you create a language-specific frontmatter override for Spanish? Answer: Add the @es suffix to the field name: title@es: Inicio. Grav applies this when the site language is set to Spanish.

  2. 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.

  3. How do you add Open Graph metadata to a page? Answer: Add an og: section in frontmatter with fields like title, description, type, url, and image. Then render them in your Twig template.

  4. What is the purpose of @dev and @prod frontmatter suffixes? Answer: They allow environment-specific overrides. For example, published@dev: false prevents a page from appearing on the development site while it is published on production.

  5. 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_image type, a custom reading_time field calculated from word count, an author hash with name, bio, and avatar URL, and a series field 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

What YAML data types can I use in frontmatter?

Strings, numbers, booleans, arrays (lists), hashes (key-value pairs), null values, and nested combinations. Dates should be quoted strings for consistent parsing.

How do I access frontmatter fields in Twig?

Use page.header.FIELDNAME. For nested fields, use dot notation: page.header.author.name. For lists, iterate with {% for item in page.header.tags %}.

{{< 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 }}\" />." >}}

What is the maximum nesting depth for frontmatter fields?

YAML technically supports unlimited nesting. For readability and performance, keep nesting to 3-4 levels. Deeper structures are harder to maintain in Twig templates.

Mini Project

Goal: Create a blog post template that uses advanced frontmatter for rich presentation.

  1. Create a blog page folder with item.md
  2. Add frontmatter with: title, date, author (name, bio, avatar), metadata (description, keywords, robots), og_image, twitter_card, reading_time, tags (list), series, custom_css
  3. 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)
  4. Create a second page with different frontmatter values
  5. Verify the Open Graph tags render correctly using a social media debugger
  6. Test language override by adding title@es and 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