Examples
EN

Data Model

Hwaro's template system centers on three core types: Site, Section, and Page. This page is a template-side reference for all properties and variables you can use when building templates. For how to write content and set front matter fields, see Writing.

Hierarchy

Site
├── Config (title, base_url, ...)
├── Pages[] (standalone pages)
├── Sections[]
│   ├── Pages[] (pages in section)
│   └── Subsections[]
│       ├── Pages[]
│       └── Subsections[] (recursive)
└── Taxonomies{}
    └── Terms{}
        └── Pages[]

Relationships

Site

The root container. Configured in config.toml.

Properties

Property Type Description
site.title String Site title
site.description String Site description
site.base_url String Base URL (no trailing slash)
site.pages Array<Page> All non-section pages
site.sections Array<Section> All section index pages
site.taxonomies Object All taxonomy groups and terms
site.data Object Data loaded from data/ directory
site.authors Object Aggregated author data
site.menus Object Named menus for the default language (see Menus)

Flat Aliases

Variable Equivalent
site_title site.title
site_description site.description
base_url site.base_url

Data Directory

Hwaro allows you to store auxiliary data in the data/ directory. Files ending in .yml, .yaml, .json, or .toml are automatically loaded and exposed via site.data.

File Structure

data/
├── authors.yml
├── products.json
├── config.toml
└── users/
    ├── alice.yml
    ├── bob.yml
    └── cho.yml

Accessing Data

Data is accessed by the filename (without extension).

For example, data/products.json:

[
  {"name": "Widget", "price": 10},
  {"name": "Gadget", "price": 20}
]

Can be accessed in templates:

{% for product in site.data.products %}
  <h2>{{ product.name }}</h2>
  <p>{{ product.price }}</p>
{% endfor %}

Subdirectories

Subdirectories under data/ become nested maps. Each file becomes a child keyed by its filename (without extension), and the parent directory itself is iterable.

Given the layout above, data/users/alice.yml, data/users/bob.yml, and data/users/cho.yml are exposed as:

{% for name, user in site.data.users %}
  <h3>{{ name }}</h3>
  <p>{{ user.bio }}</p>
{% endfor %}

Directories nest arbitrarily: data/users/admins/root.yml → site.data.users.admins.root.

Conflicts. If a directory and a file share the same stem (e.g. data/users.yml alongside data/users/), the directory wins and the file is ignored. Hwaro emits a warning during the build so the shadowed file is not silently dropped.

Remote Data Sources

site.data can also be fed from HTTP(S) endpoints, declared in config.toml. Each source is fetched once per build, before anything renders. Templates never trigger network requests, and every endpoint the build talks to is visible in one place.

For the standalone configuration guide, including cache behavior, offline error policies, timeouts, and security boundaries, see Remote Data Sources.

[[data.remote]]
key = "team"                # exposed as site.data.team
url = "https://api.example.com/team"
format = "json"             # json | toml | yaml | csv (optional, see inference below)
headers = { Authorization = "Bearer ${API_TOKEN}" }
cache = "1h"                # skip the request while the cached copy is younger than this
on_error = "fail"           # fail | warn-and-use-cache | warn-and-skip

Templates consume the result exactly like a local data file (site.data.team), so a key can move between data/team.json and a remote source without touching any template. To turn the records themselves into pages instead of feeding templates, see Content Generation.

Field Required Meaning
key yes Name under site.data. Letters, digits, _, - only; each key may be declared once. Keys are compared case-insensitively, because Team and team would share one cache file on macOS and Windows, so they are rejected as duplicates.
url yes Absolute http:// or https:// URL. Other schemes are rejected at config load.
format no json, toml, yaml, or csv. When omitted, inferred from the response Content-Type, then from the file extension of the URL the request finally landed on (after any redirects). If neither identifies a format, the fetch fails with a hint to set format explicitly.
headers no Extra request headers. Treated as credentials: never logged, and dropped when a redirect leaves the original origin.
cache no Disk-cache TTL such as "90s", "30m", "1h", "7d" (units combine: "1h30m"). While the cached copy is fresh, the build skips the request entirely. Without cache, every build refetches, but the last payload is still saved for warn-and-use-cache.
on_error no What to do when the fetch or parse fails. fail (default) aborts the build; warn-and-use-cache warns and builds from the last successfully fetched payload (any age); warn-and-skip warns and leaves site.data.<key> unset for this build.

CSV payloads parse to an array of rows, each an array of stripped string cells, the same shape load_data() produces for .csv files.

Environment variables. ${VAR} in url and headers values is replaced from the environment. A referenced variable that is not set fails the build with an error naming the variable (elsewhere in config.toml this is only a warning); write ${VAR:-default} to provide a fallback instead. The error is raised when the source is actually fetched, so commands that never fetch (hwaro deploy, hwaro new, hwaro tool ...) keep working without the variable exported.

Caching. Payloads are cached under .hwaro/remote_data/, outside data/ and every other directory hwaro serve watches, so serve rebuilds within the TTL reuse the cache instead of re-hitting the API, and cache writes never trigger a rebuild. The cache also survives hwaro build --full (only the page cache is cleared), and it is what lets an offline build succeed with on_error = "warn-and-use-cache". .hwaro/ is self-ignoring (hwaro writes .hwaro/.gitignore when creating it), so the cache never appears in git status. During a long hwaro serve session each source is fetched once and then reused in memory: an entry with a cache TTL is re-fetched on the first full rebuild after that TTL expires, and an entry without one is fetched once for the whole session rather than on every save. If a re-fetch fails while the session already holds a payload, serve warns and keeps using it, whatever on_error says, so an editor keeps working offline. hwaro build is unaffected: it is one process per build and always follows cache/on_error exactly. A changed payload invalidates cached pages the same way an edited data/ file does.

Timeouts. Each source gets 10s to connect, 30s per read, and 120s overall. The last one spans every redirect hop and the whole body, so a source that trickles bytes just inside the read timeout can't stall the build. Exceeding any of them is a fetch failure, handled by on_error like any other.

Collisions. A remote key that also exists on disk (data/team.json next to key = "team") is a config error naming both sources, since a key must have exactly one source, so site.data.team never silently depends on which one wins.

Remote sources are deliberately config-only: load_data() and shortcodes cannot take a URL. If you need more than a GET request per source (pagination, POST bodies, reshaping), fetch with a pre-build hook into data/ instead.

Site Authors

Hwaro automatically aggregates all authors defined in the authors front matter field (authors = ["id"]) into site.authors.

Defining Authors

You can enrich author data by creating a data/authors.yml (or .json, .toml) file. Keys must match the author IDs used in the page front matter.

content/my-post.md

---
title: "My Post"
authors: ["john-doe"]
---

data/authors.yml

john-doe:
  name: "John Doe"
  bio: "Creator of things."
  avatar: "/images/john.jpg"

Usage in Templates

The site.authors object contains all authors found on the site. Each author object has:

{% for id, author in site.authors %}
  <div class="author">
    <img src="{{ author.avatar }}" alt="{{ author.name }}">
    <h3>{{ author.name }}</h3>
    <p>{{ author.bio }}</p>

    <h4>Recent Posts</h4>
    <ul>
    {% for p in author.pages %}
      <li><a href="{{ p.url }}">{{ p.title }}</a></li>
    {% endfor %}
    </ul>
  </div>
{% endfor %}

Example

<title>{{ site.title }}</title>
<link rel="canonical" href="{{ site.base_url }}{{ page.url }}">

Section

A directory with _index.md that groups related content.

Properties

Property Type Description
section.title String Section title
section.description String? Section description
section.pages Array<Page> Pages in this section
section.pages_count Int Number of pages
section.list String Pre-rendered HTML list (section_list)
section.subsections Array<Section> Child sections, ordered by weight then path
section.assets Array<String> Static files in section
section.page_template String? Default template for pages
section.paginate_path String Pagination URL pattern
section.redirect_to String? Redirect URL

For the current section URL in section.html, use page.url.

Flat Aliases

Variable Equivalent
section_title section.title
section_description section.description
section_list Pre-rendered HTML list of pages

From Front Matter

Property Type Default Description
sort_by String? "date" Sort by: date (newest first), published (newest first, ignoring updated), weight (lowest first), title (A→Z)
reverse Bool? false Flip the natural sort order — see Writing › Sections › Sort direction
paginate Int? — Pages per page
transparent Bool false Pass pages to parent
generate_feeds Bool false Generate RSS feed

Iterating Pages

{% for p in section.pages %}
<article>
  <h2><a href="{{ p.url }}">{{ p.title }}</a></h2>
  <time>{{ p.date }}</time>
  {% if p.description %}
  <p>{{ p.description }}</p>
  {% endif %}
</article>
{% endfor %}

Iterating Subsections

{% for sub in section.subsections %}
<div class="category">
  <a href="{{ sub.url }}">{{ sub.title }}</a>
  <span>({{ sub.pages_count }} articles)</span>
</div>
{% endfor %}

Using section_list

For simple listings, use the pre-rendered HTML:

<ul>{{ section_list | safe }}</ul>

For custom markup, iterate section.pages directly:

<ul>
{% for p in section.pages %}
  <li>
    <a href="{{ p.url }}">{{ p.title }}</a>
    {% if p.date %}<time>{{ p.date }}</time>{% endif %}
  </li>
{% endfor %}
</ul>

Page

An individual content file (.md).

Core Properties

Property Type Description
page.title String Page title
page.description String? Page description
page.url String Relative URL path
page.permalink String? Absolute URL with base_url
page.section String Parent section name
page.date String? Publication date (YYYY-MM-DD)
page.updated String? Last updated date
page.language String Effective language code
page.translations Array<TranslationLink> Language variants
page.version Object? Documentation version (nil when unversioned) — see page.version
page.version_links Array<VersionLink> Version switcher rows (empty when unversioned)

Rendered HTML content is available as the top-level content variable.

Metadata Properties

Property Type Description
page.draft Bool Is draft
page.weight Int Sort weight
page.image String? Featured image path
page.authors Array<String> Author names
page.tags Array<String> Tags from front matter
page.taxonomies Object This page's taxonomy terms (page.taxonomies.tags, page.taxonomies.<name>)
page.extra Object Custom front matter fields

Computed Properties

Property Type Description
page.word_count Int Word count, including text pulled in by include_md, include_code and ![[note]]. Each Chinese/Japanese (Han, Hiragana, Katakana) character counts as one word; other scripts, Korean included, count space-separated words
page.reading_time Int Reading time in minutes, rounded up: ~200 words per minute, plus ~500 Chinese/Japanese characters per minute
page.summary String? Summary HTML: the chunk before <!-- more -->, else page.description, else an automatic excerpt of the rendered body ([content] summary_length, one escaped <p>). Use with | safe to embed (e.g. {{ page.summary | safe }}); for <meta name="description"> use page.description directly.
page.summary_truncated Bool true only when page.summary is an automatic excerpt that was cut short (useful for a "Read more" link).
page.assets Array<String> Static files in page bundle
page.series String Series name from front matter (empty if none)
page.series_index Int 1-based position within the series (requires [series] enabled)
page.series_pages Array<Page> All pages in the same series, sorted by series_weight
page.related_posts Array<Page> Pages sharing taxonomy terms (requires [related] enabled)
page.backlinks Array<Page> Published pages in the same language linking here, newest first (requires [content] backlinks = true; see Backlinks)
page.git Object? Commit metadata for the source file (requires [git] enabled; nil for uncommitted or generated pages). Fields: hash, short_hash, lastmod (Time), first_commit (Time), author_name, author_email — see Git Metadata

Boolean Flags

Property Type Default Description
page.toc Bool false Show table of contents
page.render Bool true Should render
page.is_index Bool — Is an index file: a section's _index.md, the home page, or a page bundle's index.md
page.is_section Bool — Is a section's _index.md. Use rejectattr("is_section") to drop subsection entries from section.pages while keeping page bundles
page.generated Bool false Auto-generated page
page.synthesized Bool false Page created by [[content.generate]]
page.in_sitemap Bool true Include in sitemap
page.in_search_index Bool true Include in search
Property Type Description
page.lower Page? Previous page in reading order
page.higher Page? Next page in reading order
page.ancestors Array<Object> Parent section chain (each entry has title and url)
page.translations Array<TranslationLink> Language variants

Custom Metadata

Property Type Description
page.extra Object Custom front matter fields

Flat Aliases

Variable Equivalent
page_title page.title
page_description page.description, else site.description
page_url page.url
page_section page.section
page_date page.date
page_image page.image, else [og] default_image
page_summary page.summary
page_word_count page.word_count
page_reading_time page.reading_time
page_permalink page.permalink
page_authors page.authors
page_tags page.tags
page_weight page.weight
page_language page.language
page_translations page.translations
lang_prefix "" for the default language, "/<code>" for other languages (e.g. {{ base_url }}{{ lang_prefix }}/posts/)
taxonomy_name Current taxonomy name (taxonomy pages)
taxonomy_term Current taxonomy term (taxonomy term pages)
content Rendered HTML content

page.lower / page.higher

Navigation follows the flat reading order across the entire site, similar to mdBook or Docusaurus. Pages are ordered depth-first through the section tree: section index → section pages → subsections (recursive). Within each section, pages are sorted by the section's sort_by setting (weight, date, or title). Pages in a directory with no _index.md come after the section tree, grouped by directory and sorted like a section's pages (by date, then path). The site root index.md always comes first.

Property Type Description
.title String Page title
.url String Page URL
.description String? Page description
.date String? Page date
<nav class="post-nav">
  {% if page.lower %}
  <a href="{{ page.lower.url }}">← {{ page.lower.title }}</a>
  {% endif %}
  
  {% if page.higher %}
  <a href="{{ page.higher.url }}">{{ page.higher.title }} →</a>
  {% endif %}
</nav>

page.ancestors

Parent sections for breadcrumbs. An ancestor section without a title (such as a transparent year folder) uses its directory name (2022), here and in the JSON-LD BreadcrumbList:

<nav class="breadcrumbs">
  <a href="/">Home</a>
  {% for ancestor in page.ancestors %}
  / <a href="{{ ancestor.url }}">{{ ancestor.title }}</a>
  {% endfor %}
  / <span>{{ page.title }}</span>
</nav>

page.version

Only on sites with [[versions.list]] (see Versioned Docs). nil for pages outside every version directory.

Property Type Description
.name String Version name (URL segment, e.g. "v2")
.label String Display label
.latest Bool Is the latest version
.url String Root URL of the version in the page's language

One row per configured version, in config order.

Property Type Description
.name String Version name
.label String Display label
.latest Bool Is the latest version
.url String Same page in that version if it exists, else that version's root
.exists Bool Counterpart page exists
.current Bool Row is the page's own version
{% for v in page.version_links %}
<a href="{{ base_url }}{{ v.url }}"{% if v.current %} aria-current="page"{% endif %}>{{ v.label }}</a>
{% endfor %}

The global versions list ({% for v in versions %}, versions.latest, versions.all) exposes every version's root URL in the current page's language.

page.translations

Property Type Description
.code String Language code (e.g., "en")
.url String Translated page URL
.title String Title in that language
.is_current Bool Current page's language
.is_default Bool Default language
{% if page.translations %}
<nav class="lang-switcher">
{% for t in page.translations %}
  {% if t.is_current %}
  <span>{{ t.code | upper }}</span>
  {% else %}
  <a href="{{ t.url }}">{{ t.code | upper }}</a>
  {% endif %}
{% endfor %}
</nav>
{% endif %}

Accessing page.extra

Custom metadata from front matter:

+++
title = "Review"

[extra]
rating = 4.5
featured = true
pros = ["Fast", "Reliable"]
+++
{% if page.extra.featured %}
<span class="badge">Featured</span>
{% endif %}

<div class="rating">{{ page.extra.rating }} / 5</div>

<ul>
{% for pro in page.extra.pros %}
  <li>{{ pro }}</li>
{% endfor %}
</ul>

A top-level outputs = ["json"] in front matter is likewise an ordinary unknown key that lands in page.extra.outputs, and it overrides the [outputs] config default for that one page/section. See Output Formats.


Time Variables

Variable Type Description
current_year Int Current year (e.g., 2025)
current_date String Current date (YYYY-MM-DD)
current_datetime String Current datetime
<footer>&copy; {{ current_year }} {{ site.title }}</footer>

SEO Variables

Pre-rendered HTML (backward compatible):

Variable Description
og_tags OpenGraph meta tags
twitter_tags Twitter Card meta tags
og_all_tags Both OG and Twitter tags
canonical_tag Canonical link tag
hreflang_tags Hreflang alternate link tags (multilingual)
pagination_seo_links <link rel="prev/next"> tags
alternate_output_tags <link rel="alternate"> tags for the page's enabled output formats
jsonld JSON-LD structured data (jsonld_article, jsonld_breadcrumb, jsonld_website, … also available; see Structured Data)
<head>
  {{ og_all_tags | safe }}
  {{ canonical_tag | safe }}
  {{ hreflang_tags | safe }}
  {{ pagination_seo_links | safe }}
</head>

Structured data for custom meta tag markup:

Property Type Description
seo.canonical_url String Full canonical URL (base_url + page URL)
seo.og_type String OpenGraph type (default: "article")
seo.og_image String Resolved absolute image URL
seo.twitter_card String Twitter card type (default: "summary_large_image")
seo.twitter_site String Twitter site handle
seo.twitter_creator String Twitter creator handle
seo.fb_app_id String Facebook App ID
seo.hreflang Array Same as page.translations

Page title, description, URL, and image are available as page.title, page.description, page.url, page.image. The seo object provides computed values specific to SEO (resolved URLs, config values).

<head>
  <link rel="canonical" href="{{ seo.canonical_url }}">
  <meta property="og:title" content="{{ page.title }}">
  <meta property="og:type" content="{{ seo.og_type }}">
  <meta property="og:url" content="{{ seo.canonical_url }}">
  {% if page.description %}
  <meta property="og:description" content="{{ page.description }}">
  {% endif %}
  {% if seo.og_image %}
  <meta property="og:image" content="{{ seo.og_image }}">
  {% endif %}
  <meta name="twitter:card" content="{{ seo.twitter_card }}">
  {% if seo.twitter_site %}
  <meta name="twitter:site" content="{{ seo.twitter_site }}">
  {% endif %}
</head>

Asset Variables

Pre-rendered <link> and <script> tags for convenience. These are generated from your config.toml settings.

Variable Description
highlight_css Syntax highlighting CSS <link> tag
highlight_js Syntax highlighting JS <script> tag
highlight_tags Both CSS and JS tags
auto_includes_css Auto-included CSS <link> tags
auto_includes_js Auto-included JS <script> tags
auto_includes All auto-include tags
math_tags KaTeX/MathJax tags for [markdown] math (empty when math is off)
mermaid_tags Mermaid script tags (empty when [markdown] mermaid is off)
pwa_tags PWA manifest link, theme-color meta, and service-worker registration (empty unless [pwa] is enabled)
search_tags Stylesheet and script tags of the built-in search UI (empty unless [search] ui = true)
<head>
  {{ highlight_css | safe }}
  {{ auto_includes_css | safe }}
</head>
<body>
  ...
  {{ highlight_js | safe }}
  {{ auto_includes_js | safe }}
</body>

Table of Contents

Only available when toc = true in front matter.

Pre-rendered HTML (backward compatible):

Variable Type Description
toc String Generated TOC HTML
toc_obj.html String Same TOC HTML in object form
{% if page.toc %}
<aside class="toc">
  {{ toc | safe }}
</aside>
{% endif %}

Structured data for custom TOC markup:

Property Type Description
toc_obj.headers Array Structured TOC header objects
toc_obj.headers[].level Int Heading level (2-6)
toc_obj.headers[].id String Anchor ID
toc_obj.headers[].title String Heading text
toc_obj.headers[].permalink String Fragment link to the heading (#id)
toc_obj.headers[].children Array Nested child headers (same structure)
{% if page.toc %}
<nav class="toc">
  <ul>
  {% for h in toc_obj.headers %}
    <li>
      <a href="{{ h.permalink }}">{{ h.title }}</a>
      {% if h.children %}
      <ul>
        {% for child in h.children %}
        <li><a href="{{ child.permalink }}">{{ child.title }}</a></li>
        {% endfor %}
      </ul>
      {% endif %}
    </li>
  {% endfor %}
  </ul>
</nav>
{% endif %}

Paginator

Available in section and taxonomy term templates when pagination is enabled (paginate in section front matter, paginate_by for taxonomies). Page 1 lives at the section URL; later pages at {url}/{paginate_path}/{n}/.

Property Type Description
paginator.paginate_by Int Items per page
paginator.base_url String Pager base URL ({url}/{paginate_path}/)
paginator.number_pagers Int Total number of pages
paginator.first String First page URL
paginator.last String Last page URL
paginator.previous String? Previous page URL (nil on first page)
paginator.next String? Next page URL (nil on last page)
paginator.pages Array<Page> Pages on the current pager
paginator.current_index Int Current page number (1-based)
paginator.total_pages Int Same as number_pagers

A pagination_obj variant exposes the same data as previous_url, next_url, first_url, last_url, current_page, total_pages, total_items, per_page, has_previous, has_next, and html (the pre-rendered nav, also available as the flat pagination variable).

{% if paginator is defined and paginator.number_pagers > 1 %}
<nav>
  {% if paginator.previous %}<a href="{{ paginator.previous }}">Prev</a>{% endif %}
  <span>{{ paginator.current_index }} / {{ paginator.number_pagers }}</span>
  {% if paginator.next %}<a href="{{ paginator.next }}">Next</a>{% endif %}
</nav>
{% endif %}

Taxonomy Variables

Available in taxonomy templates:

Variable Type Description
taxonomy_name String Taxonomy name (e.g., "tags")
taxonomy_term String Current term name (empty on the index page)
content String Pre-rendered listing HTML (terms or pages)

For custom listings, use get_taxonomy(). See Taxonomies.


site.menus exposes the default language's named menus (config [[menus.*]] + front-matter menus/menu registrations). Inside a template, prefer get_menu(name="...") over site.menus.<name>, since it resolves against the current page's language instead, falling back to the default language:

{% for item in get_menu(name="main") %}
<a href="{{ item.href }}"{% if item.url | active_path %} aria-current="page"{% endif %}>{{ item.name }}</a>
{% endfor %}

Entry Properties

Property Type Description
name String Display label
url String Bare root-relative path, or untouched external URL — comparable to page.url
href String url with the site's base_path applied (internal) or unchanged (external) — use this in <a href>
identifier String Unique key within the menu
weight Int Sort order
external Bool true for http://, https://, or // URLs
children Array\<Entry\> Nested entries whose parent matches this entry's identifier
page Page? The registering page's data (front-matter-registered entries only; nil for config-only entries and for entries registered on a section)

See Menus for the full config/front-matter reference, hierarchy, and per-language behavior.


Type Reference

Quick Reference

Type Description
String Text value
String? Optional text (may be nil)
Int Integer number
Bool true/false
Array<T> List of type T
Object Key-value map

Template Checking

{# Check for nil #}
{% if page.description %}...{% endif %}

{# Check for empty array #}
{% if page.authors %}...{% endif %}

{# Check for empty string #}
{% if page.description is present %}...{% endif %}

{# Default value #}
{{ page.description | default(value=site.description) }}

See Also