Taxonomy
The built-in taxonomy tag — turn tagged member pages into a changelog timeline, a knowledge-base grid, or a blog article list, with tag filtering, per-page tag navigation, and an RSS feed for changelog and article listings. Front-matter schema, options, and live examples.
A built-in tag that renders a listing of member pages — real pages in your content
tree that opt into a named taxonomy through front matter — in one of three shapes: a
changelog timeline, a knowledge base of categorized questions, or a blog-style
article list. Because the members are ordinary pages, every entry has its own URL and its
own Markdown body — and, unless it opts out with noindex: true, its own place in search
and llms.txt. This site’s changelog entries do opt out: they’re stealthed
(nav: false + noindex: true), and the changelog listing folds their body text into its
own search record, so it stays findable there. That fold is specific to the changelog type —
blog and knowledge-base members are ordinarily their own search landings, so a
listing there carries only their teaser; marking such a member noindex: true drops its
body from search rather than recovering it through the listing. The listing is assembled at
build time from their front matter. This site uses all three: the
Changelog tab, the Support knowledge base, and the
Blog are each one {% taxonomy %} tag over a folder of
member articles. Changelog and article listings also publish an RSS feed (pass
feed=false to opt out); knowledge-base listings do not.
Usage
Two halves: member pages declare their membership in front matter, and one listing page
renders the tag. A member joins a taxonomy with a taxonomy: entry:
---
title: Faster dev-loop rebuilds
date: 2026-05-28 09:15
taxonomy:
- name: changes
tags: [build, performance]
---
and the listing page — the one page marked listing: true — renders all members:
{% taxonomy name="changes" type="changelog" wide %}
The member’s body is the entry’s content — for type="changelog" it renders inline in
the timeline exactly as {% changelog %} renders a YAML entry’s
description, so a data-file changelog can convert to member articles with an identical
rendered result. (Island components in a member body are suppressed in the inline listing
view — they render only on the member’s own page.) Relative links in a member body are
rebased onto the member’s own URL, so they resolve the same inline as on the article page;
on a versioned site, a root-absolute link in an older version’s listing is
rewritten to that version’s equivalent page when one exists — the same in-version rule the
member’s own page follows — so the inline view never bounces a reader to the latest docs.
Front matter
Member pages
Each entry in a page’s taxonomy: list joins one taxonomy. name is required; everything
else is optional:
| Key | Effect |
|---|---|
name |
The taxonomy this page joins (required). taxonomyName is accepted as an alias — prefer name. |
tags |
Labels for this entry — a list ([a, b]) or a comma-separated string ("a, b"). These drive the filter cloud, KB categories, and tagFilter, and are listed at the bottom of the member’s own page as links into the listing’s filtered view. |
listing |
true marks this page as the taxonomy’s canonical listing page (the one carrying the {% taxonomy %} tag). |
leftnav |
true shows a taxonomy tag navigation in this page’s left sidebar (a non-clickable section heading over the tag links). dates instead shows a month-grouped member archive — the articles themselves, newest first, under “June 2026”-style headings — replacing the menu’s alphabetical entries (this site’s Blog does this). |
articleCount |
true adds per-tag article counts to those labels (as small badge pills). |
tagCloud |
true shows the taxonomy’s tag cloud in this page’s right rail. Not available on wide/full-mode pages (they have no right rail). |
featured |
true flags a knowledge-base entry as a top question, surfaced above the categories. topQuestion is accepted as an alias. |
authorName |
Blog byline shown on the article card and at the top of the article page itself (avatar + name + publish date, tucked under the title — the page’s bottom date line then keeps only “Last modified”). A dated member without an author still gets the date-only byline — this site’s changelog entries show their publish date under the title this way. |
authorAvatar |
Avatar image for the byline. Omit it and both bylines fall back to the author’s initials. |
badgeText |
A badge label on the article card. |
Several top-level front-matter keys also feed the entry: title (the entry headline),
date (YYYY-MM-DD, optionally with a time — 2026-05-28 16:30 — the full timestamp
drives the newest-first sort), description (the card / KB excerpt text), version (a
changelog release badge beside the date), and image / imageAlt (a blog card cover
image and its alt text — omit imageAlt when the cover is decorative).
The listing page
Give exactly one page listing: true for the taxonomy and put the tag in its body. A
changelog- or blog-style listing usually wants a wide layout (mode: full or
mode: wide) so it can use wide.
Options
| Attribute | Effect |
|---|---|
name="…" |
The taxonomy to render (required) — matches the members’ name. |
type="…" |
The listing shape: changelog (timeline), kb (knowledge base), or articles (blog-style cards; blog is an alias). |
tagFilter="…" |
Show only members carrying this tag (case-insensitive). A comma-separated value matches members carrying any listed tag — unless the whole value exactly matches a single tag whose own name contains a comma (tagFilter="Plans, seats & hosting"), which wins. |
cardVariant="…" |
Card look for article listings: horizontal, footer (alias withfooter), background (alias image), vertical, or plain (alias grid). |
wide |
Lay the tag cloud out in a sticky right-hand column. Requires a wide layout mode — mode: wide, full, or uncapped in the page front matter (see Modes). |
limitEntries=20 |
Show at most N entries (after the newest-first sort). limit is an alias. |
limitDays=90 |
Changelog type only — show only entries from the last N days, counted from the build date. |
combineByDay |
Changelog type only — merge all of a day’s entries into one timeline entry. The day becomes the atomic item, so the tag cloud / #tag= filter then works at day granularity (selecting a tag keeps any day that carries it, showing that day’s other entries too). |
cols=3 |
Column count for article-card grids, on desktop. Like {% cardGrid %}, the grid steps down on its own — two columns on a tablet, one on a phone — so the cards wrap instead of shrinking. |
topCount=5 |
Knowledge base only — how many featured top questions to surface (topCount=0 drops the section). |
emptyText="…" |
The no-matches message, on every listing type. Override it to localize the listing’s chrome. |
filterLabel= / clearLabel= |
Changelog/articles — the tag cloud’s “Filter by tag” heading and its “Clear” button. Override to localize. |
rssLabel="…" |
Changelog/articles — the RSS link’s accessible name and tooltip (default “RSS feed”). Override to localize. |
topLabel= / categoriesLabel= / allLabel= / showAllLabel= |
Knowledge base only — the three section headings (“Top questions”, “Browse by category”, “All articles”) and the filtered view’s “Show all” clear link (while a filter is active, the articles heading names the active tag instead of allLabel). Override to localize. |
articleLabel= / articlesLabel= |
Knowledge base only — the singular/plural noun in the “N article(s)” count line (category-card counts and the filtered status line). Override to localize the count alongside the headings above. |
banner=false |
Knowledge base only — drop the hero banner. By default a KB listing opens with a banner holding an “ask your question” field: the question goes to the site’s AI assistant when one is enabled, else it opens search pre-filled. With neither surface enabled the banner drops on its own. |
bannerTitle= / bannerText= / askPlaceholder= / askLabel= |
Knowledge base only — the banner’s heading (default “How can we help?”), optional subtitle, the ask field’s placeholder, and its submit button label. Override to localize. |
toc=false |
Changelog type only — don’t add per-entry headings to the page’s “On this page” list (added by default in a non-wide layout; article and KB listings never contribute TOC entries). |
feed=false |
Changelog and article listings only — don’t publish an RSS feed (one is emitted for each by default, advertised in the page’s <head>). KB listings never publish a feed. |
attr={…} |
Forward raw HTML attributes onto the rendered root — see below. |
A changelog listing
type="changelog" reproduces the {% changelog %} timeline — the
same component, fed from member articles instead of a YAML file. This is exactly what the
Changelog tab renders (with wide); here, capped to the three most recent
entries:
{% taxonomy name="changes" type="changelog" limitEntries=3 toc=false feed=false %}
Filter by tag
Ask AI highlights code without calling out
Opening the Ask AI panel used to fetch highlight.js from a CDN the first time a reader asked a question. It doesn’t any more: answers are highlighted by the lexer Aardvark already uses for your pages, so the assistant makes no third-party request, needs no extra Content-Security-Policy origin, and still works where that CDN is unreachable.
The colors are unchanged: answers were already painted from your theme’s syntax palette and still are. What has gone is the extra stylesheet that remapped highlight.js’s class names onto that palette, because the built-in lexer emits the same classes your own code blocks use. An answer still stays plain while it streams and is colored in one pass once it finishes — what no longer happens is the repaint when the CDN script arrived late and recolored an answer already on screen.
The trade is breadth. The built-in lexer covers 20 languages — JSON, Bash, Python, JavaScript, TypeScript, Go, PHP, Rust, Ruby, Java, C, C++, SQL, YAML, INI/TOML, CSS, SCSS, XML/HTML, Markdown and Aardvark’s own template syntax, plus the usual aliases. Anything outside that list renders as readable plain text where the CDN’s grammar set would have colored it.
Released in 0.3.3. See Ask AI.
Maps run on your own site, not a CDN
{% map %} used to pull MapLibre GL JS from a public CDN at runtime. It now bundles a
runtime served from your own site and publishes the map worker as a same-origin asset, so
nothing executable in a map comes from a third party. A map keeps working when a CDN is
blocked or slow, and it needs no CDN allowance in a Content-Security-Policy.
The runtime is read out of your own node_modules, so a site upgrading from an earlier
release has to install mantine-map 0.4.0 — add it to package.json and run your package
manager, since it is the installed tree that decides and a listed-but-uninstalled package is not
a copy Aardvark can build against. Until that copy is there, the build warns,
drops the interactive map and falls back to the accessible list of locations the tag always
renders for readers without JavaScript — the build still succeeds, which is what makes a
vanished map easy to overlook. Pin the version exactly: a copy that is not 0.4.0 is dropped the
same way, since that is the version the map surface is built against. Sites scaffolded by 0.3.3
already carry the pin. On 0.3.3 the install is required; a later release ships the runtime inside
Aardvark itself and drops the step.
Three knobs are gone with the CDN they pointed at: map.maplibreVersion, map.maplibreJs
and map.maplibreCss no longer do anything, and are ignored without a warning — delete
them rather than waiting to be told. If you set a Content-Security-Policy, a map now needs
worker-src 'self' and img-src 'self' data: blob:; the basemap’s own style, tile, glyph
and sprite origins still belong under connect-src/img-src, since those carry map data
rather than code.
The runtime is only shipped by sites that use it: a page tree with no {% map %} emits
neither the MapLibre bundle nor the worker.
Released in 0.3.3. See Map.
Ask-AI reader assistant
An optional Ask AI assistant answers reader questions grounded in your docs, backed by a metered cloud gateway. Bring your own key, or use the bundled, capped allowance to try it out.
A knowledge base
type="kb" groups members by tag into categories, with featured: true members surfaced
as top questions and each entry’s description as its excerpt. The Support
tab is one of these; here, filtered to a single category:
{% taxonomy name="support" type="kb" tagFilter="Included AI" %}
How can we help?
Top questions
By default paid AI pauses and readers fall back to free models, so you're never charged a surprise overage.
Unused allowance rolls over up to one month's worth, so allowance plus rollover never exceeds about two months at once.
All articles
The billed dollar cost of metered requests counts against your allowance, pooled account-wide and drained before your prepaid balance.
Rolled-over allowance drains first, then this month's allowance, and your prepaid balance only if you've opted into overflow.
By default paid AI pauses and readers fall back to free models, so you're never charged a surprise overage.
Unused allowance rolls over up to one month's worth, so allowance plus rollover never exceeds about two months at once.
An article list
type="articles" renders blog-style article cards
— byline, date, badge, cover image, and the description as the teaser. The
Blog tab renders all posts (with wide); here, filtered to one tag:
{% taxonomy name="blog" type="articles" tagFilter="release" feed=false %}

vark 0.9 release roundup
The 0.9 release in one read — a self-generating CLI reference, a faster dev loop, and the best of the 0.8 line.
Traversing a taxonomy yourself
The three listing types are bespoke renderings, but the underlying data is plain and
yours to walk: every {% %} block runs real Python, and two
injected helpers open the taxonomy up — taxonomy(name) returns the named
taxonomy for the current page’s language/version, and member_html(member)
renders a member’s actual body content (see below). Loop with
print() to emit any markdown (or HTML) you like:
{%
for m in taxonomy("changes").articles:
print(f"- [{m.title}]({m.url}) — {m.date_display}\n")
%}
which renders the changelog members as a plain list:
- Ask AI highlights code without calling out — August 22, 2026
- Maps run on your own site, not a CDN — August 22, 2026
- Ask-AI reader assistant — June 10, 2026
- Build-time accessibility contrast audit — June 2, 2026
- Faster dev-loop rebuilds — June 2, 2026
- Generate the CLI reference from
vark --help— June 2, 2026 - Index OpenAPI descriptions for search — June 2, 2026
- Interactive Mantine islands — June 2, 2026
- Multi-language sites — June 2, 2026
- Social unfurl cards (Open Graph) — June 2, 2026
- Standardized code-block Copy & Download buttons — June 2, 2026
The returned object exposes the taxonomy’s whole surface:
| Accessor | What you get |
|---|---|
.articles |
The member pages minus the listing page, sorted newest-first (undated members last) — each with .title, .url, .description, .tags, .date_display, .when (a datetime or None), .version, .featured, .author_name, .author_avatar, .badge_text, .image. |
.members |
The same list including the listing page (.is_listing tells them apart). |
.by_tag |
{tag: [members]} — group by tag, count per tag, build your own category index. |
.listing_url |
The canonical listing page’s URL. |
An unknown name raises at build time, listing the taxonomies that exist — the same
contract as the directive. Combine with featured/by_tag for custom surfaces the
built-ins don’t cover (“three most recent posts per tag”, a by-author archive, …).
Here’s this site’s real Support taxonomy, grouped by tag:
{%
for tag, members in sorted(taxonomy("support").by_tag.items()):
print(f"**{tag}** — {len(members)} article(s)\n")
%}
Included AI — 4 article(s) Overflow — 3 article(s) Payments & limits — 8 article(s) Plan changes — 4 article(s) Plans, seats & hosting — 8 article(s)
Member content
Front matter is only the surface — member_html(member) renders a member’s
actual body to HTML, so a loop can reproduce the articles themselves, not just
link to them. The body renders in the member’s own context (includes and
expressions resolve exactly as on the article page), the leading H1 is stripped so
your loop prints its own headline, and relative links are rebased onto the member’s
URL. Island components are suppressed with a build warning — they render only on the
member’s own page, the same contract as the inline changelog view. Calling it from
inside a member’s own body is rejected (a member embedding other members could
recurse). Here are this site’s two most recent release notes, re-rendered in full:
{%
for m in taxonomy("changes").articles[:2]:
print(f"\n#### {m.title} — {m.date_display}\n")
print(member_html(m))
%}
Ask AI highlights code without calling out — August 22, 2026
Opening the Ask AI panel used to fetch highlight.js from a CDN the first time a reader asked a question. It doesn’t any more: answers are highlighted by the lexer Aardvark already uses for your pages, so the assistant makes no third-party request, needs no extra Content-Security-Policy origin, and still works where that CDN is unreachable.
The colors are unchanged: answers were already painted from your theme’s syntax palette and still are. What has gone is the extra stylesheet that remapped highlight.js’s class names onto that palette, because the built-in lexer emits the same classes your own code blocks use. An answer still stays plain while it streams and is colored in one pass once it finishes — what no longer happens is the repaint when the CDN script arrived late and recolored an answer already on screen.
The trade is breadth. The built-in lexer covers 20 languages — JSON, Bash, Python, JavaScript, TypeScript, Go, PHP, Rust, Ruby, Java, C, C++, SQL, YAML, INI/TOML, CSS, SCSS, XML/HTML, Markdown and Aardvark’s own template syntax, plus the usual aliases. Anything outside that list renders as readable plain text where the CDN’s grammar set would have colored it.
Released in 0.3.3. See Ask AI.
Maps run on your own site, not a CDN — August 22, 2026
{% map %} used to pull MapLibre GL JS from a public CDN at runtime. It now bundles a
runtime served from your own site and publishes the map worker as a same-origin asset, so
nothing executable in a map comes from a third party. A map keeps working when a CDN is
blocked or slow, and it needs no CDN allowance in a Content-Security-Policy.
The runtime is read out of your own node_modules, so a site upgrading from an earlier
release has to install mantine-map 0.4.0 — add it to package.json and run your package
manager, since it is the installed tree that decides and a listed-but-uninstalled package is not
a copy Aardvark can build against. Until that copy is there, the build warns,
drops the interactive map and falls back to the accessible list of locations the tag always
renders for readers without JavaScript — the build still succeeds, which is what makes a
vanished map easy to overlook. Pin the version exactly: a copy that is not 0.4.0 is dropped the
same way, since that is the version the map surface is built against. Sites scaffolded by 0.3.3
already carry the pin. On 0.3.3 the install is required; a later release ships the runtime inside
Aardvark itself and drops the step.
Three knobs are gone with the CDN they pointed at: map.maplibreVersion, map.maplibreJs
and map.maplibreCss no longer do anything, and are ignored without a warning — delete
them rather than waiting to be told. If you set a Content-Security-Policy, a map now needs
worker-src 'self' and img-src 'self' data: blob:; the basemap’s own style, tile, glyph
and sprite origins still belong under connect-src/img-src, since those carry map data
rather than code.
The runtime is only shipped by sites that use it: a page tree with no {% map %} emits
neither the MapLibre bundle nor the worker.
Released in 0.3.3. See Map.
CSS Selectors
Each listing shape renders its own class-name family — target the article list, the knowledge base, the tag cloud, and the changelog timeline through their prefixes:
.aardvark-articlelist-* /* article-list layout and cards */
.aardvark-kb-* /* knowledge-base categories, top questions, entries */
.aardvark-tagcloud-* /* the tag cloud (listing column or right rail) */
.aardvark-changelog-* /* the changelog timeline (same parts as {% changelog %}) */
Injecting Attributes
attr={…} forwards raw HTML attributes — id, data-*, ARIA, analytics hooks — onto the
rendered listing root:
Filter by tag
Ask AI highlights code without calling out
Opening the Ask AI panel used to fetch highlight.js from a CDN the first time a reader asked a question. It doesn’t any more: answers are highlighted by the lexer Aardvark already uses for your pages, so the assistant makes no third-party request, needs no extra Content-Security-Policy origin, and still works where that CDN is unreachable.
The colors are unchanged: answers were already painted from your theme’s syntax palette and still are. What has gone is the extra stylesheet that remapped highlight.js’s class names onto that palette, because the built-in lexer emits the same classes your own code blocks use. An answer still stays plain while it streams and is colored in one pass once it finishes — what no longer happens is the repaint when the CDN script arrived late and recolored an answer already on screen.
The trade is breadth. The built-in lexer covers 20 languages — JSON, Bash, Python, JavaScript, TypeScript, Go, PHP, Rust, Ruby, Java, C, C++, SQL, YAML, INI/TOML, CSS, SCSS, XML/HTML, Markdown and Aardvark’s own template syntax, plus the usual aliases. Anything outside that list renders as readable plain text where the CDN’s grammar set would have colored it.
Released in 0.3.3. See Ask AI.
Maps run on your own site, not a CDN
{% map %} used to pull MapLibre GL JS from a public CDN at runtime. It now bundles a
runtime served from your own site and publishes the map worker as a same-origin asset, so
nothing executable in a map comes from a third party. A map keeps working when a CDN is
blocked or slow, and it needs no CDN allowance in a Content-Security-Policy.
The runtime is read out of your own node_modules, so a site upgrading from an earlier
release has to install mantine-map 0.4.0 — add it to package.json and run your package
manager, since it is the installed tree that decides and a listed-but-uninstalled package is not
a copy Aardvark can build against. Until that copy is there, the build warns,
drops the interactive map and falls back to the accessible list of locations the tag always
renders for readers without JavaScript — the build still succeeds, which is what makes a
vanished map easy to overlook. Pin the version exactly: a copy that is not 0.4.0 is dropped the
same way, since that is the version the map surface is built against. Sites scaffolded by 0.3.3
already carry the pin. On 0.3.3 the install is required; a later release ships the runtime inside
Aardvark itself and drops the step.
Three knobs are gone with the CDN they pointed at: map.maplibreVersion, map.maplibreJs
and map.maplibreCss no longer do anything, and are ignored without a warning — delete
them rather than waiting to be told. If you set a Content-Security-Policy, a map now needs
worker-src 'self' and img-src 'self' data: blob:; the basemap’s own style, tile, glyph
and sprite origins still belong under connect-src/img-src, since those carry map data
rather than code.
The runtime is only shipped by sites that use it: a page tree with no {% map %} emits
neither the MapLibre bundle nor the worker.
Released in 0.3.3. See Map.
{% taxonomy name="changes" type="changelog" limitEntries=2 toc=false feed=false attr={'data-analytics': 'release-feed', 'aria-label': 'Changelog'} %}
component('aardvark', 'taxonomy', name='changes', type='changelog', limitEntries=2,
toc=False, feed=False,
attr={'data-analytics': 'release-feed', 'aria-label': 'Changelog'})