Page Context

Page Hierarchy Context

Pages have access to their hierarchical relationships derived from URL structure:

PropertyTypeDescription
page.parentobject/noneParent page metadata
page.childrenlistDirect child pages (sorted by order)
page.siblingslistSibling pages with same parent
page.breadcrumbslistAncestors from root to parent
page.prevobject/nonePrevious page in sibling sequence (by order, then URL)
page.nextobject/noneNext page in sibling sequence (by order, then URL)
{% 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 %}
{% if page.parent %}
<a href="{{ page.parent.url }}" class="back-link">
    &larr; 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">
        &larr; {{ page.prev.title }}
    </a>
    {% endif %}

    {% if page.next %}
    <a href="{{ page.next.url }}" class="nav-next">
        {{ page.next.title }} &rarr;
    </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:

PropertyTypeDescription
page.word_countintWord count of the raw markdown content
page.reading_timeintEstimated reading time in minutes (200 wpm, rounded up, min 1, 0 for empty)
page.modified_datestring/noneFile modification date as YYYY-MM-DD
page.toclistTable of contents entries { level, text, id } from headings
page.statusstringPublication status from frontmatter (published, draft, review, archived)
page.noindexboolWhether 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.

PropertyTypeDescription
taxonomy.tagslistAll tags with usage counts, sorted alphabetically
taxonomy.current_tagstring/noneThe tag being viewed (only on /tags/{tag} pages)
taxonomy.pageslistPages matching the current tag (only on /tags/{tag} pages)

Each entry in taxonomy.tags has:

PropertyTypeDescription
namestringTag name (lowercase-normalized)
countintNumber 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:

RouteTemplateDescription
/tagstags.html.jinjaIndex of all tags with counts
/tags/{tag}tag.html.jinjaPages 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:

PropertyTypeDescription
titlestringPage title
urlstringPage URL path
datestring/nonePublication date
authorstring/noneAuthor name
leadstring/noneShort description/excerpt (auto-generated if not set)
tagslistList of tags
orderintSort 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:

PropertyTypeDescription
page.languagestringThe content language. Set from the filename suffix (default.de.md -> "de") or site.language. This is what belongs in <html lang>.
page.localestringThe 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.directionstringWriting 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.

PropertyTypeDescription
page.translationslistEvery available translation of this page, including the page’s own language. Empty on single-language sites.
site.languageslistAll configured language codes (display order).
site.current_languagestringThe active language for the current request.

Each entry in page.translations has:

PropertyTypeDescription
languagestringLanguage code (e.g. "de").
urlstringURL of this translation. The default language has no prefix; other languages are served under /{lang}/....
activebooltrue 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.

PropertyTypeDescription
version.currentstringCurrent version identifier (e.g., "v1.1")
version.labelstringHuman-readable label (e.g., "1.1 (Latest)")
version.badgestring/noneBadge hint (e.g., "latest", "lts")
version.rootstringThe versioning root path (e.g., "/docs")
version.is_fallbackboolWhether this page’s content came from a fallback version
version.sourcestringThe version that provides the content (differs from current when is_fallback is true)
version.alllistAll available versions with URLs for the version switcher

Each entry in version.all has:

PropertyTypeDescription
idstringVersion identifier (e.g., "v1.0")
labelstringHuman-readable label
urlstringURL to the same page in this version
is_currentboolWhether this is the currently viewed version
badgestring/noneBadge 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 lead in 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.

PropertyTypeDescription
request.pathstringURL path (e.g., /contact)
request.querymapQuery 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 %}
{# 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.

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.

PropertyTypeDescription
page.backlinkslistPages that link to this page via wikilinks

Each backlink object has:

FieldTypeDescription
urlstringURL of the linking page
titlestringTitle of the linking page
{% 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.

PropertyTypeDescription
theme.namestringTheme display name from theme.yaml.
theme.assets.css / theme.assets.jslistFiles declared in assets:.
theme.heading_anchor_stylestringHeading 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.