Page Context
This page walks through the page context by task. For the complete list of
every variable and field with its type, see Available Template
Variables in the Theming Guide,
or run accent docs template-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.