Filters and Functions

Accent CMS templates support both filters and functions to transform data and generate dynamic content.

Filters vs Functions

  • Filters transform values using the pipe syntax: {{ value | filter }}
  • Functions generate values: {{ function() }}

Available Filters

date

Formats date strings using chrono format specifiers. When no format is given, the default is %Y-%m-%d.

{# Default ISO format: 2024-01-15 #}
{{ page.date | date }}

{# Custom format: January 15, 2024 #}
{{ page.date | date("%B %d, %Y") }}

{# Short format: Jan 15 #}
{{ page.date | date("%b %d") }}

{# Year only: 2024 #}
{{ page.date | date("%Y") }}

Localized month and weekday names

The %B, %b, %A, and %a specifiers produce month and weekday names in the page’s language, so a German page renders August and März where it would otherwise render English. The language is the page’s language, then the site language, then English. Numeric specifiers never change, so an English site renders exactly as before.

Pass locale to format a date in another language regardless of the page:

{# 15. August 2026 on a German page #}
{{ page.date | date("%-d. %B %Y") }}

{# Force French: août #}
{{ page.date | date("%B", locale="fr") }}

Two presets save you from spelling out a pattern per language:

{# "long": 15. August 2026 (de), August 15, 2026 (en), 15 août 2026 (fr) #}
{{ page.date | date("long") }}

{# "short": 15.08.2026 (de), 08/15/2026 (en), 15/08/2026 (fr) #}
{{ page.date | date("short") }}

Names come from the Unicode CLDR data compiled into Accent. A language with no data falls back to English and logs a warning once.

Three limits are worth knowing:

  • The names are the stand-alone (nominative) forms. A language that inflects a month name inside a sentence – Russian, Polish, Czech, or Finnish, for example – gets the nominative form, which can be grammatically off in a running date. This matches how other static site generators behave.
  • The long and short presets use day-month-year order (month-day-year for English). On a year-first locale such as Japanese, Chinese, or Korean, write an explicit pattern instead of the preset.
  • Only the name specifiers %B, %b, %A, and %a are localized. Composite specifiers such as %c keep their English names.

Supported input date formats:

FormatExample
ISO 86012024-01-15
US style01/15/2024
European style15.01.2024
Long monthJanuary 15, 2024
Short monthJan 15, 2024
ISO with time2024-01-15T10:30:00
ISO with time in UTC2024-01-15T10:30:00Z
ISO with an offset2024-01-15T10:30:00+02:00
ISO with fractional seconds2024-01-15T10:30:00.000Z
Date and time with a space2024-01-15 10:30:00
An offset with no colon or no minutes2024-01-15 10:30:00 +0100, +01
No seconds2024-01-15T10:30, 2024-01-15T10:30Z
A trailing UTC2024-01-15 10:30:00 UTC

A timestamp prints in its own zone, never converted to the zone of the machine that builds the site, and one with no zone reads as UTC. So site.built_at, which is in UTC, formats the same on a laptop and on a CI runner, and time and zone tokens work on it:

{{ site.built_at | date("%d.%m.%Y %H:%M %z") }}

A pattern chrono cannot write, such as one with an unknown token, prints the value unchanged.

If the date string cannot be parsed, the original value is returned unchanged, and the build logs a warning naming the value and the template, once for each. An empty value, such as a page with no date, is not logged.

truncate

Truncates text to a specified length with ellipsis.

{{ page.content | truncate(100) }}

Example output: This is a very long content string that will be...

The length is counted in characters, not bytes, so an accented Latin letter, a CJK character, and an emoji each count as one no matter how many bytes they occupy – truncate(60) on a German or Japanese title keeps 60 characters. Text already at or below the limit is returned unchanged, with no ellipsis appended.

The cut falls at exactly that character position, so it can land inside a word. There is no killwords argument.

slugify

Converts text to URL-friendly slugs.

{{ page.title | slugify }}
InputOutput
Hello Worldhello-world
My Blog Post!my-blog-post
Rust & MiniJinjarust-minijinja

Use cases:

  • Generating anchor IDs: <h2 id="{{ page.title | slugify }}">
  • Creating CSS classes: <article class="post-{{ page.title | slugify }}">

Available Functions

now()

Returns the current datetime, in UTC, as an object with date and time components. It prints as 2026-10-07 17:44:20, and the date filter formats it: {{ now() | date("%d.%m.%Y %H:%M") }}.

Properties:

PropertyDescriptionExample
.year4-digit year2026
.monthMonth number (1-12)1
.dayDay of month (1-31)25
.hourHour (0-23)14
.minuteMinute (0-59)30
.secondSecond (0-59)45

Usage Examples:

Display the current year in a footer:

<footer>
    &copy; {{ now().year }} {{ site.name }}
</footer>

Display the full date:

<p>Generated: {{ now().year }}-{{ now().month }}-{{ now().day }}</p>

Display date and time:

<time>{{ now().year }}-{{ now().month }}-{{ now().day }} {{ now().hour }}:{{ now().minute }}</time>

Common patterns:

{# Copyright notice with dynamic year #}
<footer>&copy; 2025-{{ now().year }} My Company</footer>

{# Build timestamp #}
<meta name="build-date" content="{{ now().year }}-{{ now().month }}-{{ now().day }}">

{# Conditional content based on time #}
{% if now().hour >= 6 and now().hour < 12 %}
    Good morning!
{% elif now().hour >= 12 and now().hour < 18 %}
    Good afternoon!
{% else %}
    Good evening!
{% endif %}

media(path, page_url?)

Generates URLs for serving media files . Supports two media locations: a shared site-wide library and page-local files co-located with content.

Arguments:

ArgumentRequiredDescription
pathYesFile path. Absolute (starts with /) for shared media, relative for page-local
page_urlNoCurrent page URL for resolving page-local paths. Use page.url

Usage Examples:

Shared media (site-wide assets in site/media/):

{# Absolute path -> /media/logos/logo.svg #}
<img src="{{ media('/logos/logo.svg') }}" alt="Logo">

{# Shared PDF #}
<a href="{{ media('/docs/whitepaper.pdf') }}">Download PDF</a>

Page-local media (files alongside content markdown):

{# Relative path + page.url -> /content-media/blog/my-post/hero.jpg #}
<img src="{{ media('hero.jpg', page.url) }}" alt="{{ page.title }}">

{# Gallery image next to the page #}
<img src="{{ media('gallery/photo1.jpg', page.url) }}">

Using with custom frontmatter fields:

---
title: My Post
hero_image: hero.jpg
---
{% if page.custom.hero_image is defined %}
<img src="{{ media(page.custom.hero_image, page.url) }}" alt="{{ page.title }}">
{% endif %}

The media() function returns a safe string that is not HTML-escaped, so it can be used directly in src and href attributes without the | safe filter. On a sub-path deployment the returned URL carries site.base_path automatically (passing the already-prefixed page.url is fine), so media links keep working without changes.

Two more inputs come back usable, so a component’s media attribute can go straight to media():

  • A path that already starts with /media/, with or without the sub-path prefix, is returned as that URL rather than prefixed again.
  • A remote file, an http or https URL, a scheme-relative //host/... URL or a data: URI, is returned as written. Any other scheme is treated as a path.

A component template has no page, so pass only an absolute media value to media(), and a page-relative one to url(): {{ media(attrs.image) if attrs.image is startingwith("/") else url(attrs.image) }}.

responsive_image(path, alt, sizes?)

Generates a responsive <img> tag with a srcset at each configured breakpoint , so the browser downloads the smallest image that fills the layout slot. It builds on the image processing pipeline, which serves each width on demand.

Arguments:

ArgumentRequiredDescription
pathYesMedia URL or path. Combine with media() for page-local images
altYesAlt text for the image
sizesNoThe sizes attribute hint (defaults to 100vw)

Usage Examples:

Shared media:

{{ responsive_image('/photos/hero.jpg', 'Mountain at sunrise') }}

Page-local media (resolve the path through media() first):

{{ responsive_image(media('hero.jpg', page.url), page.title, '(max-width: 768px) 100vw, 50vw') }}

From a custom frontmatter field:

---
title: My Post
responsive_hero: hero.jpg
---
{% if page.custom.responsive_hero is defined %}
{{ responsive_image(media(page.custom.responsive_hero, page.url), page.title, '(max-width: 768px) 100vw, 50vw') }}
{% endif %}

The generated tag includes loading="lazy" and a src at the middle breakpoint as a fallback for browsers that ignore srcset. The candidate widths come from the media processing configuration. Returns a safe HTML string.

video_embed(url, width?, height?)

Turns a YouTube or Vimeo URL into a privacy-respecting <iframe> embed . YouTube URLs become youtube-nocookie.com embeds and Vimeo URLs get dnt=1 (do-not-track), so neither platform sets tracking cookies until the visitor plays the video. Unrecognized URLs are embedded as-is, which is useful for direct embed URLs.

For self-hosted video files stored alongside your content, use video() instead.

Arguments:

ArgumentRequiredDescription
urlYesA YouTube, Vimeo, or direct embed URL
widthNoiframe width attribute, passed as a keyword (default 560)
heightNoiframe height attribute, passed as a keyword (default 315)

Usage Examples:

{# YouTube -> youtube-nocookie.com embed #}
{{ video_embed('https://youtube.com/watch?v=dQw4w9WgXcQ') }}

{# Vimeo with a custom size (width/height are keyword arguments) #}
{{ video_embed('https://vimeo.com/123456789', width='800', height='450') }}

From a custom frontmatter field:

---
title: My Post
video_embed: https://youtube.com/watch?v=dQw4w9WgXcQ
---
{% if page.custom.video_embed is defined %}
{{ video_embed(page.custom.video_embed, width='800', height='450') }}
{% endif %}

Returns a safe HTML string. The default theme demonstrates this with a .video-embed figure that keeps the iframe in a responsive 16:9 box regardless of the width/height attributes.

recent_documents(limit, model?)

Returns the most-recently-updated pages across the entire site as a list, newest first . Use it to build a “Recently updated” sidebar, a footer block, or a homepage feed.

Arguments:

ArgumentRequiredDescription
limitYesMaximum number of pages to return
modelNoRestrict the list to a single document model (e.g. "news")

Sort key: each page is ordered by its frontmatter date when present, falling back to the file’s last-modified date (modified_date) when no date is set. Pages with neither are omitted. Dates are compared as ISO YYYY-MM-DD strings, so an explicit date always takes precedence over file modification time – which matters because operations like git clone, git checkout, or a full rebuild reset every file’s modification time at once.

Each returned entry is a page listing object exposing the usual fields: title, url, date, modified_date, lead, author, tags, and model.

Usage:

{# Last 5 updated pages, any type #}
<aside class="recent-docs">
  <h4>Recently updated</h4>
  <ul>
    {% for p in recent_documents(5) %}
      <li>
        <a href="{{ p.url }}">{{ p.title }}</a>
        <time datetime="{{ p.date or p.modified_date }}">{{ p.date or p.modified_date }}</time>
      </li>
    {% endfor %}
  </ul>
</aside>

Filter to a single document model:

{# Last 5 updated "news" pages only #}
{% for p in recent_documents(5, "news") %}
  <a href="{{ p.url }}">{{ p.title }}</a>
{% endfor %}

The function reads the in-memory page index, so it works identically in accent serve and accent build, and renders nothing when no pages have a usable date. The default theme ships a ready-made partials/recent-documents.html.jinja that wraps this function – include it with {% include "partials/recent-documents.html.jinja" %}.

Looking for the most-viewed pages rather than the most recently updated? That requires per-request view tracking and is a separate, server-only capability; recent_documents() ranks by update time only.

Returns the page hierarchy as a nested tree . The root node is the home of the rendered page’s language: the site’s / page, or /en on an English page of a site with several languages (a placeholder when there is no home page). Link the home with nav_tree().url, not url('/'). Every node carries url, title, nav_title, and children, in the same order as all_pages (menu.order, then URL). Use it to render a site-wide navigation with one recursive loop instead of scanning all_pages once per level.

Usage:

<ul>
{% for node in nav_tree().children recursive %}
  <li><a href="{{ node.url }}">{{ node.nav_title or node.title }}</a>
  {% if node.children %}<ul>{{ loop(node.children) }}</ul>{% endif %}</li>
{% endfor %}
</ul>

To expand only the branch that leads to the current page, call loop(node.children) only for the node whose url is page.url and for the nodes whose url is in page.breadcrumbs. The built-in viewer theme’s base.html.jinja does exactly that.

On a site with several languages, the tree is the rendered page’s language’s. A German page’s tree holds the German pages, rooted at /, and an English page’s tree holds the English pages, rooted at the English home, /en. A page in a language the site does not list gets the default language’s tree.

A page’s parent is its hierarchy_url with the last segment removed, the same rule behind page.parent, page.children, and page.breadcrumbs. That key is the directory-derived URL every listing entry carries, so a page whose public URL a permalink pattern or a frontmatter url: moved still sits where its directory put it; every node’s url is the public one, joined with the deployment base path. A page whose parent has no page of its own hangs off its nearest ancestor that does, so every page stays reachable. The tree is built once per render from the in-memory page index, so it works identically in accent serve and accent build.

json_ld()

Returns pre-computed JSON-LD structured data HTML for the current page . The output is a safe HTML string containing one or more <script type="application/ld+json"> blocks, or an empty string when structured data is disabled.

Usage:

{# In <head> - recommended approach #}
<head>
    {{ json_ld() }}
</head>

The function reads the structured data that Accent CMS generates automatically based on page type (BlogPosting, TechArticle, WebPage, etc.). No arguments are needed.

The legacy syntax {% if page.json_ld %}{{ page.json_ld | safe }}{% endif %} also works via page.json_ld.

url(path)

Joins a root-absolute path with the site’s deployment path prefix (site.base_path) . Available as both a function and a filter. This is the helper that keeps a theme portable when the site is deployed under a sub-path, such as a GitHub Pages project site served from https://user.github.io/repo/.

Usage:

<a href="{{ url('/') }}">Home</a>
<a href="{{ url('/blog') }}">Blog</a>
<link rel="stylesheet" href="{{ '/theme/assets/css/main.css' | url }}">

With site.base_path set to /repo, url('/tags') renders /repo/tags; with no base path configured it is the identity function, so the same theme works for root and sub-path deployments.

  • Context URLs (page.url, collection entries, crumb.url, media URLs) already carry the prefix, so they need no wrapping – and the join is idempotent, so url(page.url) is safe.
  • Already-absolute (http://, https://), protocol-relative (//host/...), relative paths, and #anchors pass through unchanged.
  • Returns a safe string, so it can be used directly in href/src.
  • In a component template, url() returns the path without the prefix, and so do media(), tag_url(), cdn_url() and the other helpers that join it; the context URLs are root-relative there too. Accent adds the prefix to every root-relative href, src and poster in a component’s output when it places the output in the page, so a helper that added it too would write it twice. absolute_url() keeps the prefix, since an absolute URL is never prefixed again.

Wrap every hardcoded root-absolute href/src/action in your page templates with url(). In a component template, Accent prefixes href and src itself, and nothing prefixes an action, so build that one from site.base_path. When a base path is set, accent build warns about hardcoded root-absolute URLs it finds in theme templates and fails the build if any unprefixed internal URL reaches the output. See Theme Portability for the full rule and Static Build Deployment for the GitHub Pages recipe.

tag_url(tag?)

Returns the URL of a tag’s page, or of the tag index when called with no argument. Use it for every tag link: the URL is the tag’s slug (Javascript Runtime links /tags/javascript-runtime), carries the deployment prefix as url() does, and on a site with several languages points at the tag pages of the language of the page being rendered (/de/tags/javascript-runtime).

Usage:

{% for tag in page.tags %}<a href="{{ tag_url(tag) }}">{{ tag }}</a>{% endfor %}
{% for tag in taxonomy.tags %}<a href="{{ tag_url(tag.name) }}">{{ tag.label }}</a>{% endfor %}
<a href="{{ tag_url() }}">All tags</a>

Returns a safe string, so it can be used directly in href.

term_url(taxonomy, term?)

Returns the URL of a configured taxonomy’s term page, or of the taxonomy’s index when called without a term: what tag_url() is for tags, for any taxonomy. The first argument is the taxonomy’s name; the term is a name or a taxonomy.<name> entry. The URL is the term’s slug under the taxonomy’s configured path, carries the deployment prefix, and points at the pages of the language of the page being rendered. term_url('tags', tag) equals tag_url(tag).

Usage:

{% for term in taxonomy.categories %}
  <a href="{{ term_url('categories', term) }}">{{ term.label }} ({{ term.count }})</a>
{% endfor %}
<a href="{{ term_url('categories') }}">All categories</a>

On a taxonomy’s own pages, taxonomy.name is the taxonomy being rendered, so term_url(taxonomy.name, term) in tags.html.jinja or tag.html.jinja links the tags on the tag pages and a configured taxonomy’s terms on its pages.

Returns a safe string, so it can be used directly in href.

feed_outputs()

Returns the configured feed outputs (the default is the single RSS feed at /feed.xml), one entry per output with path, mime_type, format (rss/atom/json), and a human label for link titles. Empty while feed.enabled is false, so a theme that loops over it unconditionally advertises no feed that does not exist. The path has no language prefix; prefix the page’s language yourself for a non-default language, as the default theme does.

Usage:

{% for feed in feed_outputs() %}
<link rel="alternate" type="{{ feed.mime_type }}"
    title="{{ site.name }} {{ feed.label }}" href="{{ absolute_url(feed.path) }}">
{% endfor %}

absolute_url(url)

Makes a URL absolute by prepending site.url . Available as both a function and a filter, and used by the bundled head-meta partial for canonical links, og:url, og:image, and twitter:image.

Usage:

{{ absolute_url(page.url) }}      {# https://example.com/blog/post/ #}
{{ page.url | absolute_url }}     {# filter form, same result #}
  • Root-relative paths gain the site origin (a trailing slash on site.url is trimmed).
  • Already-absolute URLs (http://, https://) pass through unchanged.
  • When site.url is empty the input is returned unchanged.
  • Under a sub-path deployment the result carries site.base_path exactly once, whether the input is a raw path or an already-prefixed context URL – absolute_url('/feed.xml') on a site at https://user.github.io/repo renders https://user.github.io/repo/feed.xml.
  • The result is a safe string, as url() returns, so it goes straight into an href or content attribute, or an XML feed or sitemap, with its slashes literal and no | safe. A quote or a tag delimiter in the URL is percent-encoded, so it cannot end the attribute, and & is written as &amp;. It does not check where the URL points: a javascript: value passes through, so check a link an author supplies before you write it into an href.

Unlike a request-based canonical, absolute_url() does not depend on the request, so it produces identical output in accent serve and static accent build. See Social Sharing and SEO Metadata for the full head-meta system.

cdn_url(path)

Rewrites an asset path to your configured CDN origin . Use it for stylesheets, scripts, images, and any other static asset you want served from the CDN instead of the application origin.

Usage:

<link rel="stylesheet" href="{{ cdn_url('/assets/css/main.css') }}">
<script nonce="{{ nonce }}" src="{{ cdn_url('/assets/js/app.js') }}"></script>
<img src="{{ cdn_url('/media/hero.webp') }}" alt="Hero">

When CDN integration is enabled, cdn_url('/assets/css/main.css') returns https://cdn.example.com/assets/css/main.css (using your configured cdn.base_url). When CDN is disabled, it returns the path joined with the site’s deployment path prefix (site.base_path, empty for root deployments) – the same behavior as url() – so the same templates work whether or not a CDN is configured, on root and sub-path deployments alike.

  • Root-relative and relative paths gain the CDN origin (a trailing slash on base_url is trimmed).
  • Already-absolute (http://, https://) and protocol-relative (//host/...) URLs pass through unchanged.
  • Produces identical output in accent serve and static accent build, so generated HTML already points at the CDN.
  • Returns a safe string, so it can be used directly in href/src without | safe.

CDN integration is a Pro feature, and enabling it for accent serve --production requires a Pro license. The cdn_url() helper itself is always available (as the identity passthrough above) so themes stay portable across editions. Markdown image URLs are rewritten automatically when CDN is enabled — you only need cdn_url() for assets referenced in templates. See CDN Integration for the full configuration.

font_css_url(family)

Generates a stylesheet URL for a web font family . With the CDN font proxy enabled, fonts are fetched server-side and served from your own origin under /_fonts/, so visitor browsers never contact Google directly — eliminating the IP-address leak that European courts have ruled a GDPR violation.

Usage:

<link rel="stylesheet" href="{{ font_css_url('Inter:wght@400;700') }}">
  • With the font proxy enabled, this renders a same-origin path like /_fonts/css/inter-wght-400-700.css that works identically in serve and static build.
  • With the proxy disabled, it falls back to a direct https://fonts.googleapis.com/css2?... URL, so the helper is safe to use on any edition.
  • List the families you want proxied under cdn.font_proxy.families so static builds can pre-download them.

Emit the <link rel="alternate" type="text/plain" href="/llms.txt"> tag that points LLM crawlers (ChatGPT, Perplexity, Gemini, and others) at the site’s machine-readable overview .

Usage:

<head>
    {{ llms_discovery_link() }}
</head>

When llms.enabled is true, the template engine also auto-injects the same tag just before </head> on every rendered HTML page in both accent serve and accent build. Calling this function explicitly is therefore optional – the engine detects the existing href="/llms.txt" and skips its automatic placement to avoid duplicates. Use the explicit form when the theme wants control over the precise position of the tag in the head (for example, to keep all alternate-link declarations grouped together).

The function takes no arguments and returns a safe HTML string.

debug(value)

Outputs the debug representation of any template value. Useful for troubleshooting and understanding the structure of template variables.

Usage:

{{ debug(page) }}
{{ debug(page.tags) }}
{{ debug(site) }}

Output example:

{
    "title": "Hello World",
    "url": "/docs/hello-world",
    "date": "2026-01-25",
    "tags": [
        "rust",
        "web"
    ]
}

Debugging Tips:

Use debug() to:

  • Inspect the structure of page, site, or theme objects
  • Verify frontmatter values are parsed correctly
  • Check if a variable exists before using it
  • Understand what data is available in your template

Conditional debugging with dev.debug:

Use the built-in dev.debug flag to conditionally show debug output:

{% if dev.debug %}
<pre class="debug-panel">
{{ debug(page) }}
</pre>
{% endif %}

Enable debug mode in config.yaml:

dev:
  debug: true  # Enable template debug output

Quick debug with ?acdbg query parameter:

For quick debugging without modifying templates, add ?acdbg to any page URL:

https://localhost:4400/docs?acdbg
https://localhost:4400/docs?acdbg=right
https://localhost:4400/docs?acdbg=float

This displays a panel showing all template context values (site, page, version, custom frontmatter, theme, pages, all_pages, dev, taxonomy, config, search, cache) with syntax highlighting. The panel supports three position modes (bottom, right, float) and can be resized by dragging the edge handle. Position preferences persist in localStorage. The panel only appears in dev mode and is hidden in production.

Development Context (dev)

The dev object exposes development settings to templates:

PropertyTypeDescription
dev.debugboolWhether debug mode is enabled
dev.hot_reloadboolWhether hot reload is enabled
dev.browser_reloadboolWhether browser auto-reload is enabled

Usage examples:

{# Show debug panel only in debug mode #}
{% if dev.debug %}
<div class="debug-panel">
    <h4>Debug Info</h4>
    <pre>{{ debug(page) }}</pre>
</div>
{% endif %}

{# Show development indicators #}
{% if dev.hot_reload %}
<div class="dev-indicator">Hot reload active</div>
{% endif %}

Build Information Context

The accent object exposes version and build information:

PropertyTypeDescription
accent.versionstringSemantic version (e.g., 0.6.0)
accent.git_hashstringShort git commit hash (e.g., abc1234)
accent.build_timestringUTC build timestamp
accent.version_stringstringVersion with hash (e.g., 0.6.0 (abc1234))

Usage example:

<footer>
    Powered by Accent CMS {{ accent.version_string }}
</footer>