Page Context
Page Hierarchy Context
Pages have access to their hierarchical relationships derived from URL structure:
| Property | Type | Description |
|---|---|---|
page.parent | object/none | Parent page metadata |
page.children | list | Direct child pages (sorted by order) |
page.siblings | list | Sibling pages with same parent |
page.breadcrumbs | list | Ancestors from root to parent |
page.prev | object/none | Previous page in sibling sequence (by order, then URL) |
page.next | object/none | Next page in sibling sequence (by order, then URL) |
Breadcrumb Navigation
{% if page.breadcrumbs %}
<nav class="breadcrumbs" aria-label="Breadcrumb">
{% for crumb in page.breadcrumbs %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
<span>/</span>
{% endfor %}
<span>{{ page.title }}</span>
</nav>
{% endif %}
Children Listing
List child pages with their lead/excerpt:
{% if page.children %}
<section class="children">
<h2>In this section</h2>
<ul>
{% for child in page.children %}
<li>
<a href="{{ child.url }}">{{ child.title }}</a>
{% if child.lead %}<p>{{ child.lead }}</p>{% endif %}
</li>
{% endfor %}
</ul>
</section>
{% endif %}
Parent Link
{% if page.parent %}
<a href="{{ page.parent.url }}" class="back-link">
← Back to {{ page.parent.title }}
</a>
{% endif %}
Siblings Navigation
{% if page.siblings %}
<nav class="siblings">
<h4>Related pages</h4>
<ul>
{% for sib in page.siblings %}
<li><a href="{{ sib.url }}">{{ sib.title }}</a></li>
{% endfor %}
</ul>
</nav>
{% endif %}
Previous/Next Navigation
Navigate between sibling pages in order:
<nav class="page-nav">
{% if page.prev %}
<a href="{{ page.prev.url }}" class="nav-prev">
← {{ page.prev.title }}
</a>
{% endif %}
{% if page.next %}
<a href="{{ page.next.url }}" class="nav-next">
{{ page.next.title }} →
</a>
{% endif %}
</nav>
Pages are ordered by menu.order (primary), then URL (secondary) – the same order
page.siblings and every other listing of those pages uses, so stepping through with
prev/next visits exactly what a listing shows. The first page has no prev, and the
last page has no next. Pages a reader cannot see (archived, or drafts outside
dev.show_drafts) are skipped rather than ending the sequence.
Table of Contents
The page.toc list contains heading entries extracted from the markdown content. Each entry has level (1-6), text (plain heading text), and id (anchor slug). Headings in the rendered HTML automatically receive id attributes matching these slugs, enabling in-page navigation.
{% if page.toc %}
<nav class="toc">
<h3>On this page</h3>
<ul>
{% for entry in page.toc %}
<li class="toc-level-{{ entry.level }}">
<a href="#{{ entry.id }}">{{ entry.text }}</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
Duplicate headings produce disambiguated IDs with numeric suffixes (e.g., introduction, introduction-1).
Page Computed Metadata
The current page exposes computed metadata useful for blog posts and documentation:
| Property | Type | Description |
|---|---|---|
page.word_count | int | Word count of the raw markdown content |
page.reading_time | int | Estimated reading time in minutes (200 wpm, rounded up, min 1, 0 for empty) |
page.modified_date | string/none | File modification date as YYYY-MM-DD |
page.toc | list | Table of contents entries { level, text, id } from headings |
page.status | string | Publication status from frontmatter (published, draft, review, archived) |
page.noindex | bool | Whether the page asked to stay out of indexes (noindex: true in frontmatter) |
<div class="meta">
<span>{{ page.reading_time }} min read</span>
<span>{{ page.word_count }} words</span>
{% if page.modified_date %}
<span>Updated {{ page.modified_date }}</span>
{% endif %}
</div>
noindex is a typed frontmatter field, so it is page.noindex and not
page.custom.noindex – the custom map carries only the keys the frontmatter
struct does not claim, so a template looking for it there finds nothing and
silently indexes the page. The same flag already excludes the page from
sitemap.xml, feed.xml and the llms.txt pair; reading it in the head is
what turns the intent into something a crawler acts on:
{% if page.status in ["draft", "review", "archived"] or page.noindex %}
<meta name="robots" content="noindex,nofollow">
{% endif %}
Taxonomy Context (Tags)
The taxonomy object is available on every page, providing tag data for navigation. On the virtual /tags and /tags/{tag} routes, additional fields are populated.
| Property | Type | Description |
|---|---|---|
taxonomy.tags | list | All tags with usage counts, sorted alphabetically |
taxonomy.current_tag | string/none | The tag being viewed (only on /tags/{tag} pages) |
taxonomy.pages | list | Pages matching the current tag (only on /tags/{tag} pages) |
Each entry in taxonomy.tags has:
| Property | Type | Description |
|---|---|---|
name | string | Tag name (lowercase-normalized) |
count | int | Number of pages with this tag |
Tag cloud on any page:
{% if taxonomy.tags %}
<nav class="tag-cloud">
{% for tag in taxonomy.tags %}
<a href="/tags/{{ tag.name }}">{{ tag.name }} ({{ tag.count }})</a>
{% endfor %}
</nav>
{% endif %}
Tag listing page (/tags/{tag}):
<h1>Posts tagged "{{ taxonomy.current_tag }}"</h1>
{% for p in taxonomy.pages %}
<article>
<a href="{{ p.url }}">{{ p.title }}</a>
{% if p.date %}<time>{{ p.date }}</time>{% endif %}
{% if p.lead %}<p>{{ p.lead }}</p>{% endif %}
</article>
{% endfor %}
Virtual Tag Routes
Accent CMS automatically generates two virtual routes for tag navigation:
| Route | Template | Description |
|---|---|---|
/tags | tags.html.jinja | Index of all tags with counts |
/tags/{tag} | tag.html.jinja | Pages matching a specific tag |
These routes require corresponding templates in your theme. The default theme includes both templates. Tags are case-insensitive (Rust and rust are the same tag). Per-tag page lists are sorted by date, newest first.
To add tag support to your theme, create tags.html.jinja and tag.html.jinja in your theme’s templates/ directory.
Page Metadata Properties
Each page in children, siblings, breadcrumbs, and parent has:
| Property | Type | Description |
|---|---|---|
title | string | Page title |
url | string | Page URL path |
date | string/none | Publication date |
author | string/none | Author name |
lead | string/none | Short description/excerpt (auto-generated if not set) |
tags | list | List of tags |
order | int | Sort order (from menu.order) |
Page Locale and Smart Punctuation
Pages can override the typographic locale used for curly-quote remapping by setting locale: <code> in their frontmatter. This is read directly via page.frontmatter.locale (or page.locale in templates that flatten custom fields) and takes precedence over the site-wide markdown.locale config. The remap itself runs in the markdown pipeline before HTML rendering, so templates do not need to do anything special for it to take effect – {{ page.content }} already contains the locale-correct quote forms.
A frontmatter example:
--- title: Bonjour le monde locale: fr ---
Templates have three locale-shaped fields, each with a distinct purpose:
| Property | Type | Description |
|---|---|---|
page.language | string | The content language. Set from the filename suffix (default.de.md -> "de") or site.language. This is what belongs in <html lang>. |
page.locale | string | The typographic locale used for smart-quote remapping. Resolved by frontmatter locale > markdown.locale (site config) > page.language > "en". Useful for debug panels or to know which curly-quote table was applied. Because markdown.locale is site-wide, do not use this for <html lang> on multilingual sites – prefer page.language. |
page.direction | string | Writing direction, computed from page.language (content language). "rtl" for Arabic and Hebrew, "ltr" for everything else (CJK included). Safe to read directly: the template-context builder defaults to "ltr" for fixture/error pages. |
Setting lang and dir on the root element:
<html lang="{{ page.language or site.current_language or site.language }}" dir="{{ page.direction or 'ltr' }}">
The or chain handles pages built outside the regular pipeline (taxonomy index, custom error pages) where page.language may be empty. site.current_language is the per-request language from the multilingual routing layer, so the chain keeps per-page language correct on multilingual sites.
The full mapping table for supported locales lives in Markdown Extensions: Smart Punctuation.
Page Language and Translations
On a multilingual site (two or more entries in site.languages), every page
also carries the list of its available translations. This is the data a language
switcher is built from.
| Property | Type | Description |
|---|---|---|
page.translations | list | Every available translation of this page, including the page’s own language. Empty on single-language sites. |
site.languages | list | All configured language codes (display order). |
site.current_language | string | The active language for the current request. |
Each entry in page.translations has:
| Property | Type | Description |
|---|---|---|
language | string | Language code (e.g. "de"). |
url | string | URL of this translation. The default language has no prefix; other languages are served under /{lang}/.... |
active | bool | true for the language currently being displayed. |
Build a switcher (or include the bundled partials/language-switcher.html.jinja):
{% if page.translations | length > 1 %}
<ul class="language-switcher" aria-label="Language">
{% for t in page.translations %}
<li>
{% if t.active %}
<span class="active" lang="{{ t.language }}">{{ t.language | upper }}</span>
{% else %}
<a href="{{ t.url }}" lang="{{ t.language }}" hreflang="{{ t.language }}">{{ t.language | upper }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
The lang and hreflang attributes (matching the bundled partial) tell
assistive technology and search engines which language each option is in.
See the Multi-Language Content guide for the full i18n model: enabling languages, authoring translations, the URL scheme, and per-language feeds.
Version Context
Pages within a versioning root receive a version context variable. For non-versioned pages, version is null.
| Property | Type | Description |
|---|---|---|
version.current | string | Current version identifier (e.g., "v1.1") |
version.label | string | Human-readable label (e.g., "1.1 (Latest)") |
version.badge | string/none | Badge hint (e.g., "latest", "lts") |
version.root | string | The versioning root path (e.g., "/docs") |
version.is_fallback | bool | Whether this page’s content came from a fallback version |
version.source | string | The version that provides the content (differs from current when is_fallback is true) |
version.all | list | All available versions with URLs for the version switcher |
Each entry in version.all has:
| Property | Type | Description |
|---|---|---|
id | string | Version identifier (e.g., "v1.0") |
label | string | Human-readable label |
url | string | URL to the same page in this version |
is_current | bool | Whether this is the currently viewed version |
badge | string/none | Badge hint |
Version Dropdown
The default theme includes a version switcher partial. Include it in any template:
{% include "partials/version-dropdown.html.jinja" %}
Or build a custom switcher using version.all:
{% if version and version.all | length > 1 %}
<nav aria-label="Version">
{% for v in version.all %}
{% if v.is_current %}
<strong>{{ v.label }}</strong>
{% else %}
<a href="{{ v.url }}">{{ v.label }}</a>
{% endif %}
{% endfor %}
</nav>
{% endif %}
Fallback Notice
Show a notice when content is inherited from an older version:
{% if version.is_fallback %}
<div class="notice">
This page has not been updated for {{ version.label }}.
You are viewing the {{ version.source }} version.
</div>
{% endif %}
See the Content Workflow guide for setup and configuration.
Auto-Excerpt Generation
When a page does not have a lead field in its frontmatter, Accent CMS automatically extracts the first paragraph of the markdown content as a plain-text excerpt. This means listing templates that use child.lead will always have a description to show, even without a manually-written lead.
The auto-excerpt:
- Skips headings and blank lines to find the first paragraph
- Strips raw HTML, so a page opening with a layout block (
<header>,<div>, a hero) is described by its visible words rather than its tags - Strips inline markdown formatting (bold, italic, links, code, images)
- Truncates to 300 characters with
...if the paragraph is long - Is overridden by an explicit
leadin frontmatter (manual lead always wins)
HTML you are documenting is kept: markup inside a fenced code block, an
indented code block, or an inline code span survives the strip, so a page
teaching <section> keeps it in the excerpt while a page merely laid out with
one does not.
Because the strip runs before the paragraph is chosen, an opening block that
contains no visible text at all (<div class="hero"></div>) is skipped
entirely and the excerpt comes from the first real sentence below it.
page.lead is plain text whether it was derived or you wrote it. A lead:
containing HTML has its tags removed on the way out, because a lead is a
description of the page and every surface that publishes one – meta tags,
social cards, feed items, llms.txt, the JSON APIs, listings – has nowhere
to render markup.
Your file is not touched: only the published view is stripped, so what you
typed is what stays in the frontmatter and what the editor shows you. As
everywhere else, a tag written inside `backticks` survives, so you can
still document one in a lead.
Request Context
During accent serve, templates have access to the current HTTP request’s path and query parameters via the request object. This is useful for form validation feedback, filter/sort UI, and campaign tracking.
| Property | Type | Description |
|---|---|---|
request.path | string | URL path (e.g., /contact) |
request.query | map | Query parameters as key-value pairs |
In build mode (accent build), request is undefined (falsy), since static sites have no HTTP request.
Reading Query Parameters
{# Show a success message after form submission #}
{% if request.query.status == "sent" %}
<div class="alert success">Thank you! Your message has been sent.</div>
{% endif %}
{# Show an error message #}
{% if request.query.error %}
<div class="alert error">Please check your input and try again.</div>
{% endif %}
Using in Links
{# Highlight the active sort option #}
<a href="{{ page.url }}?sort=date"
class="{{ 'active' if request.query.sort == 'date' }}">
Sort by date
</a>
Build Mode Safety
Templates that use request.query work in both serve and build modes without conditionals. Accessing a property on undefined returns undefined in MiniJinja, which is falsy:
{# Safe in both serve and build mode -- no error if request is undefined #}
{% if request.query.status == "sent" %}
<div class="alert success">Message sent!</div>
{% endif %}
Percent Decoding
Query parameter values are automatically percent-decoded. ?name=Ren%C3%A9 becomes request.query.name = Rene (with accent). The + character is decoded as a space.
Backlinks
When pages link to each other via [[wikilinks]], Accent builds a reverse index so each page knows which other pages link to it. This is exposed as page.backlinks.
| Property | Type | Description |
|---|---|---|
page.backlinks | list | Pages that link to this page via wikilinks |
Each backlink object has:
| Field | Type | Description |
|---|---|---|
url | string | URL of the linking page |
title | string | Title of the linking page |
Displaying Backlinks
{% if page.backlinks %}
<aside class="backlinks">
<h4>Linked from</h4>
<ul>
{% for backlink in page.backlinks %}
<li><a href="{{ backlink.url }}">{{ backlink.title }}</a></li>
{% endfor %}
</ul>
</aside>
{% endif %}
Backlinks are available in both accent serve and accent build modes. The index is rebuilt automatically when content changes.
CLI
Query backlinks for any page via the command line:
accent query backlinks /docs/getting-started
Returns a JSON array of {url, title} objects representing pages that link to the given URL.
Theme Context
Templates also receive a theme object exposing values from the active theme’s theme.yaml.
| Property | Type | Description |
|---|---|---|
theme.name | string | Theme display name from theme.yaml. |
theme.assets.css / theme.assets.js | list | Files declared in assets:. |
theme.heading_anchor_style | string | Heading anchor preset: one of copy (default), gutter, hash, pilcrow. |
Heading Anchor Presets
The renderer emits an empty <a class="heading-anchor" href="#id"> after every heading; the visible marker (chain icon, #, pilcrow, or nothing) is supplied by theme CSS based on heading_anchor_style. Layouts wire the preset through to CSS via a body-level data attribute and conditionally load the click-to-copy script:
<body data-heading-anchor-style="{{ theme.heading_anchor_style }}">
...
{% if theme.heading_anchor_style == "copy" %}
<script src="/theme/assets/js/heading-anchor-copy.js" defer></script>
{% endif %}
</body>
Each theme stylesheet provides four scoped preset blocks:
[data-heading-anchor-style="copy"] .heading-anchor::after { /* chain icon */ } [data-heading-anchor-style="gutter"] .heading-anchor::after { /* same chain icon, no JS */ } [data-heading-anchor-style="hash"] .heading-anchor::after { content: "#"; } [data-heading-anchor-style="pilcrow"] .heading-anchor::after { content: "\00B6"; }
Set heading_anchor_style: hash in theme.yaml to fall back to the legacy appended #. Pages that should look like marketing copy rather than docs can set anchors: false in their frontmatter to suppress the anchor element entirely; heading id attributes and the page.toc are still emitted, so external #fragment links and TOC sidebars keep working.