--- 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}

{#insert /}
``` **Root layout** (`templates/layouts/default.html`): ```html {@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)}

{=post.title}

{=post.description}

{/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 !} Link Docs {! 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 ``, `<meta>` author/description, Open Graph and Twitter card tags. Also emits `<meta name="robots">` when the page frontmatter defines a `robots:` value - `{#rss site /}` — adds the RSS `<link>` 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.