---
name: quarkus-roq-frontmatter
description: "Create a website from your Markdown/Asciidoc/Html pages using FrontMatter headers (url, layout, seo, data). Can be used standalone with a running Quarkus app (hybrid mode, SSR) or as part of the full Roq SSG."
guide: https://docs.quarkiverse.io/quarkus-roq/dev/quarkus-roq-frontmatter.html
---
# Quarkus Roq FrontMatter
Roq FrontMatter is the template engine behind Roq. It processes Markdown, AsciiDoc, and HTML pages with YAML frontmatter headers into web pages using Qute templates. It can be used standalone with a running Quarkus app (hybrid mode, SSR) or as part of the full Roq SSG.
**If your project uses the full `quarkus-roq` extension**, also see the [quarkus-roq skill](https://raw.githubusercontent.com/quarkiverse/quarkus-roq/main/roq/deployment/src/main/resources/META-INF/quarkus-skill.md) for additional features: directory structure differences, static generation, CLI commands, themes, LLMs.txt, and testing.
### Directory Structure
When used standalone (without the full `quarkus-roq` extension), directories are under `src/main/resources/`. With the full Roq SSG, they are at the **project root** instead (see the quarkus-roq skill).
```
src/main/resources/
content/ # Pages and collections
index.html # Site index page (required)
about.md # Standalone page
posts/ # Collection directory
2024-08-29-my-post.md # Date-based document
data/ # JSON/YAML data files
authors.yaml
templates/ # Qute layout templates
layouts/ # Layout files
main.html
partials/ # Reusable template fragments
header.html
public/ # Static assets served as-is
images/ # Default image directory
```
### FrontMatter Pages
Pages in `content/` use YAML frontmatter between `---` delimiters. Supported formats: `.md` (Markdown), `.html`, `.adoc` (AsciiDoc).
```yaml
---
title: "My Page Title"
description: "Page description for SEO"
layout: page
slug: custom-url-slug
image: photo.jpg
date: 2024-08-29 13:32:20 +0200
tags: [blogging, quarkus]
author: ia3andy
robots: noindex
draft: false
paginate:
collection: posts
size: 10
link: posts/page-:page
redirect_from: [old-url, another-old-url]
---
Page content here (Markdown, HTML, or AsciiDoc)
```
Key fields:
- **title** — Page title (falls back to source path if missing)
- **description** — Used for SEO meta tags
- **layout** — Qute layout template to wrap this page. Resolves local first, then theme fallback (e.g. `page`, `post`). Use `theme-layout:` to explicitly target a theme layout
- **slug** — Custom URL slug. Used by the `:slug` placeholder (used in the default collection link template `/:collection/:slug/`) and also by `:path` (used in the default page link template `/:path:ext`). If not set, `:slug` falls back to the title, then filename; `:path` uses the filename
- **image** — Page image. Can be a full URL, a filename from `public/images/`, or an attached file name (for directory pages). Do NOT include the `images/` prefix, just use the filename (e.g. `image: photo.jpg`, not `image: images/photo.jpg`)
- **date** — Page date. For collection documents, can also be parsed from filename (`YYYY-MM-DD-slug.md`)
- **tags** — String or array of tags
- **author** — Author identifier
- **lang** — Language/locale of the page (e.g. `fr`, `en-US`). Drives the rendered `` attribute and the `og:locale` SEO meta tag. Resolution order: page `lang` → site `lang` (set in the index page frontmatter) → the JVM default locale. This is independent of `site.defaultLocale`, which only controls the fallback locale used for locale-aware date formatting (`page.date.shortDate`, etc.)
- **robots** — Value rendered as `` in the HTML head by the built-in `{#seo page site /}` tag (e.g. `noindex`, `nofollow`, `noindex, nofollow`). Use it to keep drafts, internal docs, or staging pages out of search engine indexes. The meta tag is only emitted when the key is set on the page
- **draft** — `true` to mark as draft (hidden unless `site.draft=true`)
- **paginate** — Enable pagination. Shorthand: `paginate: posts`. Full config: `collection`, `size`, `link`
- **redirect_from** / **aliases** — Old URLs that redirect to this page (requires aliases plugin)
### Layouts
Layouts are Qute templates in `templates/layouts/` that wrap page content using `{#insert /}`.
**Layout file** (`templates/layouts/main.html`):
```html
---
layout: default
---
{@io.quarkiverse.roq.frontmatter.runtime.model.Page page}
{@io.quarkiverse.roq.frontmatter.runtime.model.Site site}
{=page.title}
{#seo page site /}
{#rss site /}{! adds the RSS tag !}
{#insert head /}
{#insert /}
```
**Layout chain**: A page with `layout: post` → post layout has `layout: main` → main has `layout: default` → default is the root (no layout field). Each level uses `{#insert /}` where child content goes.
**IMPORTANT**: Use `{#insert /}` in layouts to render child content. NEVER use `{=page.content}` in layouts — that causes recursive rendering issues.
### Collections
Collections group related documents (e.g. blog posts). The default collection is `posts` with layout `post`.
**Configuration** (`application.properties`):
```properties
site.collections.posts.layout=post
site.collections.posts.future=false
site.collections.posts.hidden=false
# Custom collection
site.collections.recipes.layout=page
site.collections.recipes.hidden=false
```
### Collections from Data
Generate collection pages from data files instead of content files using `from-data`. Each entry in the data produces a page rendered with the specified layout.
**From a single data file** (`data/events.yml`):
```yaml
- id: hello-lads
name: "First event"
description: "This is the first event"
- id: roq-and-roll
name: "SSG FTW"
description: "Static site generation with Roq"
```
```properties
site.collections.events.layout=page-event
site.collections.events.from-data.id-key=id
```
**From a data directory** (`data/events/first.yml`, `data/events/second.yml`):
```properties
site.collections.events.layout=page-event
site.collections.events.from-data.id-key=_key
```
Both single files (array) and data directories work with `from-data`. For directories, each file in the directory becomes a page. `_key` is the object key (filename without extension). The `id-key` field specifies which data field to use as the page identifier (like a filename for content-based collections). The id-key value is slugified.
By default, the data source name matches the collection id. Use `from-data.name` to point to a different data source:
```properties
site.collections.highlights.layout=page-highlight
site.collections.highlights.from-data.id-key=id
site.collections.highlights.from-data.name=events
```
This lets multiple collections share the same data source with different layouts.
The layout template accesses data fields via `page.data`:
```html
{=page.data.id}
{=page.data.name}
{=page.data.description}
```
**File naming**: Documents in collection directories use date-based names: `YYYY-MM-DD-slug.md` (e.g. `2024-08-29-welcome-to-roq.md`). The date is extracted from the filename.
**Default URL resolution**: For collection documents, URLs are derived from the `slug` frontmatter field (if set), then the `title` (slugified), then the filename. For example, a file `content/posts/2024-08-29-my-post.md` with `title: "My First Post"` will be served at `/posts/my-first-post`. Standalone pages in `content/` use their filename as the path (e.g. `content/about.md` becomes `/about`); if a `slug` is set in frontmatter, `:path` will use it in place of the filename while preserving the directory structure. Default link templates can be configured globally: `site.page-link` for non-collection pages (default `/:path:ext`) and `site.collections..link` for collection documents (default `/:collection/:slug/`). These are overridden by an explicit `link` in the page's frontmatter or layout.
**Iterating**:
```html
{#for post in site.collections.posts}
{=post.title}
{/for}
```
**Document navigation**:
```html
{#if page.previous}
Previous: {=page.previous.title}
{/if}
{#if page.next}
Next: {=page.next.title}
{/if}
```
**Collection methods** (`RoqCollection`):
- `site.collections.list` — list all collections
- `posts.by(key...)` — retrieve non-null values by frontmatter keys
- `posts.group(key...)` — group documents by frontmatter field values
- `posts.featured(n)` — first N documents
- `posts.rest(n)` — documents after the first N
- `posts.filter(key, value)` — documents matching a frontmatter key/value
- `posts.future` / `posts.past` — filter by date
- `posts.sortBy(key, reverse)` — sort by frontmatter key
- `posts.sortByDate(reverse)` — sort by date
### Pagination
Enable pagination in a page's frontmatter:
```yaml
---
paginate: posts
# Or full config:
paginate:
collection: posts
size: 10
link: posts/page-:page
---
```
**Iterating paginated documents**:
```html
{#for post in site.collections.posts.paginated(page.paginator)}
{/for}
```
**Pagination controls** (built-in partial):
```html
{#include fm/pagination.html}
{#newer}{/newer}
{#older}{/older}
{/include}
```
**Paginator properties** (available as `page.paginator`):
- `paginator.collection` — collection name
- `paginator.collectionSize` — total documents
- `paginator.limit` — docs per page
- `paginator.total` — total number of pages
- `paginator.currentIndex` — current page (1-based)
- `paginator.firstUrl` — URL of first page
- `paginator.previous` / `paginator.prev` — previous page URL (null if first)
- `paginator.next` — next page URL (null if last)
- `paginator.pagesUrl` — list of all page URLs
- `paginator.isFirst` / `paginator.isSecond` — boolean checks
### Template Variables
**`site` object** (`Site`):
- `site.url` — site root URL (`RoqUrl`)
- `site.url(path)` — resolve path relative to root. Also `site.url(path, path1)`, `site.url(path, path1, path2)`
- `site.title` — from index page frontmatter `title`
- `site.description` — from index page frontmatter `description`
- `site.image` — default site image from frontmatter `image`/`img`/`picture`. Already resolves from `public/images/`. Use directly: `{=site.image}`, never `{=site.image('/images/...')}` or `{=site.image('images/...')}`
- `site.image(name)` — resolve image by filename only (e.g. `site.image('logo.png')`, not `site.image('/images/logo.png')`)
- `site.imageExists(name)` — check if image exists
- `site.data` — site-level frontmatter data (`JsonObject`)
- `site.pages` — all normal pages (no collection documents)
- `site.collections` — all collections (e.g. `site.collections.posts`)
- `site.allPages` — all pages including documents
- `site.index` — the site index page
- `site.page(sourcePath)` — find page by source path (returns any page type)
- `site.normalPage(sourcePath)` — find normal page only
- `site.document(sourcePath)` — find document page only
- `site.file(name)` — resolve file from `public/`
- `site.fileExists(name)` — check if public file exists
- `site.files` — list of all public static files
- `site.pageContent(page)` — render a page's inner content
**`page` object** (`Page`):
- `page.url` — page URL (`RoqUrl`)
- `page.title` — from frontmatter `title`
- `page.description` — from frontmatter `description`
- `page.image` — page image (`RoqUrl`). Already resolves from `public/images/` or attached files. Use directly: `{=page.image}`, never `{=page.image('/images/...')}` or `{=page.image('images/...')}`
- `page.image(name)` — resolve specific image by filename only (e.g. `page.image('photo.jpg')`, not `page.image('/images/photo.jpg')`)
- `page.imageExists(name)` — check if image exists
- `page.date` — page date (`ZonedDateTime`, null for normal pages without a date; always set for collection documents)
- `page.data` — all frontmatter data (`JsonObject`)
- `page.data(name)` — get specific frontmatter value
- `page.content` — rendered inner content (without layouts), use only outside of layouts to avoid recursion
- `page.contentAbstract` — first 75 words of rendered content (HTML stripped)
- `page.contentAbstract(n)` — first N words of rendered content (HTML stripped)
- `page.rawTemplate` — the raw generated Qute template for this page
- `page.sourcePath` — source file relative path (e.g. `posts/my-post.md`)
- `page.sourceFileName` — file name only
- `page.baseFileName` — file name without extension
- `page.id` — unique identifier (source path)
- `page.draft` — whether page is a draft
- `page.files` — attached files (directory pages only)
- `page.file(name)` — resolve attached file URL
- `page.fileExists(name)` — check if attached file exists
- `page.source` — page source info (`PageSource`), e.g. `page.source.isTargetHtml`
- `page.site` — reference back to `Site`
**`page` for collection documents** (`DocumentPage` extends `Page`):
- `page.collectionId` — collection name
- `page.collection` — the `RoqCollection`
- `page.next` / `page.nextPage` — next document in collection
- `page.previous` / `page.prev` / `page.previousPage` / `page.prevPage` — previous document
- `page.hidden` — whether document is hidden
**`RoqUrl`** (URL object):
- `{=url}` or `{=url.path}` — relative path (e.g. `/posts/my-post/`), encoded
- `{=url.absolute}` — full URL (e.g. `https://example.com/posts/my-post/`)
- `{=url.relative}` — same as `path`
- `{=url.encoded}` — URL-encoded absolute URL
- `{=url.resolve(path)}` or `{=url.join(path)}` — join with another path
- `{=url.append(str)}` — concatenate without `/`
- `{=url.isExternal}` — boolean
- `{=url.fromRoot(path)}` — resolve from app root
### Qute Syntax Essentials
> **Note:** Examples in this guide use the alternative expression syntax (`{=expr}`), which is enabled by default in new Roq projects. See [Alternative Expression Syntax](#alternative-expression-syntax) for details.
```html
{! Expressions !}
{=page.title}
{=page.data.customField}
{=page.date.format('yyyy, MMM dd')}
{! Conditionals !}
{#if page.image}...{/if}
{#if page.data.author??}...{/if}
{! Loops !}
{#for post in site.collections.posts}...{/for}
{#for tag in page.data.tags.asStrings}...{/for}
{! Include partial !}
{#include partials/header /}
{! Layout insertion point (in layouts) !}
{#insert /}
{#insert head /}
{#insert menu}{#include partials/sidebar-menu /}{/}
{! URL resolution !}
LinkDocs
{! CDI beans (data files) !}
{=cdi:authors.ia3andy.name}
{#for item in cdi:contributors.contributors}{=item.name}{/for}
{! Let bindings !}
{#let author=cdi:authors.get(page.data.author)}
{=author.name}
{/let}
{! Slugify !}
{#let tagSlug=tag.slugify}
{=tagSlug}
{/let}
{! Type declarations (in layouts) !}
{@io.quarkiverse.roq.frontmatter.runtime.model.Page page}
{@io.quarkiverse.roq.frontmatter.runtime.model.Site site}
{@io.quarkiverse.roq.frontmatter.runtime.model.DocumentPage page}
{! With alt syntax, curly braces are plain text !}
{not-evaluated} is just plain text
{=page.title} is evaluated
```
### Alternative Expression Syntax
Qute supports an alternative expression syntax where output expressions use `{=expr}` instead of `{expr}`. This makes templates safer when content contains curly braces (e.g. code samples, JSON), since only `{=...}` and `{#...}` are interpreted as Qute expressions. Everything else is plain text.
Enable it in `application.properties`:
```properties
quarkus.qute.alt-expr-syntax=true
```
With alt syntax enabled:
- Expressions: `{=page.title}`, `{=site.url}`, `{=post.description}`
- Sections (unchanged): `{#for ...}`, `{#if ...}`, `{#include ...}`, `{@type ...}`
- Plain text: `{anything}` is NOT interpreted (no escaping needed)
Standard Qute syntax is still the default, but this will change in a future version. New projects already have alt syntax enabled via the generated config. We recommend setting the config explicitly so your project is ready.
### Data Files
Place JSON or YAML files in the `data/` directory. Each file becomes a CDI bean accessible in templates.
**Example** (`data/authors.yaml`):
```yaml
ia3andy:
name: Andy
url: https://github.com/ia3andy
```
**Template access**:
```html
{=cdi:authors.ia3andy.name}
{#let author=cdi:authors.get(page.data.author)}
{=author.name}
{/let}
```
**Type-safe mapping** (Java):
```java
@DataMapping("authors")
public record Authors(Map authors) {
public record Author(String name, String url) {}
}
```
### Built-in Tags
Add to root layout ``:
```html
{#seo page site /}
{#rss site /}
{#favicon site /}
{#ga4 /}
```
- `{#seo page site /}` — generates ``, `` author/description, Open Graph and Twitter card tags. Also emits `` when the page frontmatter defines a `robots:` value
- `{#rss site /}` — adds the RSS `` tag to the HTML head (does not generate the feed itself). Included by the base theme layout and all built-in themes
- `{#favicon site /}` — auto-discovers favicon files from `public/` (favicon.svg, .ico, .png, apple-touch-icon.png)
- `{#ga4 /}` — Google Analytics 4 (configure `analytics.ga4` in site index frontmatter)
### Template Extensions
Date formatting (on `ZonedDateTime`):
- `{=page.date.iso}` — ISO 8601 (`2024-08-29T13:32:20+02:00`)
- `{=page.date.isoDate}` — date only (`2024-08-29`)
- `{=page.date.shortDate}` / `{=page.date.longDate}` — locale-aware date
- `{=page.date.shortDateTime}` / `{=page.date.longDateTime}` — locale-aware date-time
- `{=page.date.rfc822}` — RFC 822 (for RSS)
- `{=page.date.format('yyyy, MMM dd')}` — custom pattern
Content helpers:
- `{=text.slugify}` — URL-friendly slug
- `{=htmlContent.stripHtml}` — remove HTML tags
- `{=text.numberOfWords}` — word count
- `{=text.wordLimit(n)}` — truncate to N words
- `{=page.readTime}` — estimated reading time in minutes
- `{=page.contentAbstract}` / `{=page.contentAbstract(n)}` — first N words (default 75)
- `{=list.randomise}` — shuffle a list
- `{=fileName.mimeType}` — MIME type from extension
### Configuration
```properties
site.url=http://localhost:8080
site.draft=true # Show draft pages
site.future=true # Show future-dated documents
site.defaultLocale=en # Default locale for date formatting (see also 'lang' frontmatter key)
site.draftDirectory=drafts # Folder name for drafts
site.slugifyFiles=true # Slugify static file names for SEO
site.escaped-pages=posts/escaped** # Skip Qute parsing for matched pages
site.path-prefix=/blog # Serve Roq pages under a sub-path
```
### Hybrid Mode
In standalone mode, Roq FrontMatter works alongside a running Quarkus app. Pages are server-rendered by the Quarkus HTTP server. Use `quarkus dev` to start dev mode. Combine with REST endpoints, WebSockets, and other Quarkus features. Use `site.path-prefix` to serve pages under a sub-path.
### Qute Reference Guide
For advanced Qute template needs (e.g. `@TemplateExtension` to add custom methods to objects, type-safe templates, template globals, or complex expressions), consult the full [Qute reference guide](https://quarkus.io/guides/qute-reference).
### Common Pitfalls
- **Layout inheritance** — ALWAYS use the frontmatter `layout:` field for layout inheritance (e.g. `layout: main`). NEVER use Qute's `{#include}` or `{#layout}` directives for this purpose — they won't work with Roq's layout chain.
- **`{=page.content}` in layouts** — NEVER use `{=page.content}` in layouts. Use `{#insert /}` to render child content. `{=page.content}` causes recursive rendering.
- **Wrong directory location** — Standalone Roq FrontMatter uses `src/main/resources/` for `content/`, `templates/`, `public/`, `data/`. The full `quarkus-roq` extension uses project root instead.
- **Layout resolution** — `layout: page` resolves local first, then theme fallback (themes require full `quarkus-roq`). Use `theme-layout: page` to explicitly target the theme layout.
- **Date format in filenames** — Collection documents must use `YYYY-MM-DD-slug.md` format for date extraction.
- **Image resolution** — Images can be: a full URL (`https://...`), a filename resolved from `public/images/`, or an attached file name for directory-based pages. Do NOT use the `images/` prefix in frontmatter or template calls (e.g. `image: photo.jpg`, not `image: images/photo.jpg`).
- **Escaping Qute** — With alt syntax enabled, plain `{expression}` is not evaluated, so escaping is not needed. Without alt syntax, use `\{expression}` to escape Qute expressions that should be rendered literally. You can also add pages to `site.escaped-pages` config to skip Qute parsing entirely.