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:

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.