Juicer
English

Template data

The full data context exposed to templates — site, page, section, content, toc.

Every template renders against a single map. The top-level keys are documented below; nested fields follow.

.site

The merged site config (site.toml overlaid on the baseline) plus a few computed extras:

KeyTypeWhat
.site.<config>variesEvery key from site.toml
.site.tocList[TOCItem]Site-wide auto-nav (only if no nav is set)
.site.startString?URL of the conventional landing page
.site.pagesList[Map]Every page’s enriched record
.site.pagesByPathMap[String, Map]Same records, keyed by relPermalink
.site.postsList[Map]Dated, non-section, non-static pages, newest first (see below)
.site.pagesByYearList[Map]Same posts grouped by year, year descending (see below)
.site.rootMap?The root section’s _index record (or null)
.site.tagsList[Term]Every distinct tag used in the site (see below)
.site.categoriesList[Term]Same shape as .site.tags, for the categories axis
.site.authorsList[Map]Every author with at least one referencing page — registry record + id, url, count, pages
.site.authorRegistryList[Map]The raw [[authors]] table from site.toml in declaration order — includes authors with zero referencing pages, useful for staff-directory layouts (see below)
.site.authorsPathStringURL prefix under which the team listing and per-author archives are emitted. Configured by authorsPath in site.toml; defaults to /authors/. See below
.site.nowMapBuild-time “now” timestamp in four shapes (see below)
.site.eventsList[Map]Pages in the configured eventsSection with explicit date: frontmatter, sorted ascending. Includes future-dated events (see below)
.site.calendarList[Map]Pre-computed N months of calendar grid starting at the current month, with weekly recurring events expanded onto every matching weekday (see below)
.site.photosList[Map]Aggregated photos: frontmatter entries from every page, sorted by parent-page date descending (see below)
.site.dataMap[String, Any]Nested namespace of structured data loaded from data/ files (see below)
.site.commentsMap[String, Any]?The [comments] config block from site.toml (provider + provider-specific keys), or absent if no [comments] table is set (see below)

Term shape

Every entry in .site.tags and .site.categories (and on tag-archive templates, the elements of .terms) has these fields:

KeyTypeWhat
nameStringThe original tag name as it appeared in frontmatter
slugStringURL-safe form (lowercased, ASCII-folded, non-alnum runs collapsed to -)
urlStringSite-relative archive URL — /tags/<slug>/ or /categories/<slug>/
countIntHow many pages reference this term
pagesList[Map]The pages themselves, each in the same shape as a .site.pages entry

.site.posts and .site.pagesByYear

Curated views of the post stream — the same data, sliced two ways.

.site.posts is the flat list, newest first. A page is included if it has an explicit parsed date: in frontmatter, is not a section index (_index.md), and is not flagged static: true. Filesystem-mtime fallback dates do not count — only authored dates make it in. The result is exactly the set of pages a blog or news site would call “the posts.”

.site.pagesByYear groups the same list by calendar year, with years descending and pages within each year still newest-first:

KeyTypeWhat
yearIntThe 4-digit year
countIntPosts in this year
pagesList[Map]The posts themselves, each shaped like a .site.pages entry

Useful for chronological archive pages (<h2>2024</h2> then a list).

.site.authorRegistry

The raw [[authors]] table from site.toml, preserved in declaration order. Each entry is the verbatim TOML table — id, name, role, bio, avatar, email, plus any custom fields you’ve added.

Distinct from .site.authors, which is filtered to authors who have at least one referencing page and is augmented with count, url, and pages. Use .site.authorRegistry for staff-directory layouts that should show every team member regardless of how many sermons or posts they’ve authored; use .site.authors for “browse by author” archives.

When /authors/ and /authors/<id>/ get rendered

The two layouts have different gates so themes can ship a “team page” that shows everyone, even on a fresh site with no posts:

  • layouts/_default/author-list.html/authors/index.html is rendered whenever a registry exists at all ([[authors]] non-empty in site.toml). The layout typically iterates .site.authorRegistry.
  • layouts/_default/author-page.html/authors/<id>/index.html is only rendered for authors with at least one referencing page. An empty per-author archive page would be misleading.

Either layout is opt-in — missing the file means the corresponding output is silently skipped.

Renaming the URL prefix — authorsPath

By default both the team listing and the per-author archives live under /authors/. Sites using a non-blog theme often want a different label — a cafe wants /team/, a doc site wants /people/. Set authorsPath in site.toml:

authorsPath = "/team/"

The engine pivots three things in lockstep: the on-disk output directory, the url field on every .site.authors term, and the .site.authorsPath string surfaced for templates. Themes should read {{ .site.authorsPath }} rather than hard-coding /authors/ so a site-level override propagates without a layout edit.

Forgiving normalization: team, /team, team/, and /team/ all become /team/. Path traversal segments (., ..) are rejected with a warning and the default is used.

.site.now

Build-time timestamp captured once per build, in four shapes so the right one is always at hand:

KeyTypeWhat
isoStringFull ISO offset — 2026-05-09T14:00:00Z
dateStringYYYY-MM-DD, lex-comparable with .page.dateShort for past/future filters
longStringMay 9, 2026, same formatter as .page.dateLong
yearIntUseful for footer copyright

The static-site model means “now” is legitimately frozen at build time. Live-reload re-runs the build on every change, so the value stays fresh during local preview.

.site.events

Pages in the configured events section (eventsSection in site.toml, default "events") with an explicit date: frontmatter, sorted ascending. Each entry is shaped like a .site.pages record — title, url, date, dateShort, summary, plus any frontmatter the event page declares (eventTime, eventLocation, recurring, etc.).

Includes future-dated events: events are announced before they happen, and the engine’s site-wide future-post filter is exempted for pages in the events section so both .site.events entries and the corresponding event detail pages survive the build. Future-dated posts (anything outside the events section) continue to be future-skipped — see the --future CLI flag to override per build.

Recurring events appear once each (the canonical page record); use .site.calendar for occurrence-by-occurrence expansion.

.site.calendar

Pre-computed N months (default 12, override via calendarMonths in site.toml) starting at the current month. Each month is:

KeyTypeWhat
yearInt4-digit year
monthInt1–12
monthNameString"May"
labelString"May 2026"
weeksList[List[Cell]]Always six 7-day rows, Sunday-first

Each cell:

KeyTypeWhat
dayInt?1–31, or null for out-of-month padding cells
isTodayBooleantrue for the cell whose year/month/day matches .site.now
eventsList[Map]Basic page records for events on this day (empty list when none)

Recurring weekly events (recurring: weekly + optional recurringDay:) are expanded onto every matching weekday from their start date forward. A recurring event with no recurringDay: defaults to its start-date’s day of the week. Non-recurring events appear once on their date.

The grid is always 6×7 = 42 cells per month so calendar templates don’t have to special-case month boundaries; out-of-month cells render as day=null padding.

.site.photos

Aggregated photos: frontmatter entries from every page on the site, sorted by the parent page’s date descending. Each entry:

KeyTypeWhat
srcStringImage URL (absolute or site-relative)
captionStringDisplay caption — empty string when not provided
altStringAccessibility text — falls back to caption when absent
pageMapBasic page record of the parent page (lets templates link the photo back to its event/album/sermon)

Frontmatter shape — either a list of strings (URLs only) or a list of maps with src / caption / alt keys:

photos:
  - "/img/picnic-01.jpg"
  - { src: "/img/picnic-02.jpg", caption: "The pie table" }

Pages without a photos: frontmatter contribute nothing.

.site.data

Structured data the theme or site author wants templates to reach for without forcing it into every page’s frontmatter. juicer scans the dataDir (default data/) under the site root and under each active theme:

data/team.toml              → .site.data.team
data/menu/lunch.yaml        → .site.data.menu.lunch
data/menu/dinner.toml       → .site.data.menu.dinner

The file’s basename (sans extension) becomes the leaf key; each directory under data/ becomes a nesting level. Both .toml and .yaml / .yml are accepted — pick whichever shape fits the data better. JSON is not parsed natively, but since it’s a strict subset of YAML you can rename .json.yaml and it works.

A typical use:

# data/team.toml# data/team.toml
[[members]]
name = "Alice"
role = "Lead"

[[members]]
name = "Bob"
role = "Eng"
{{ for m <- .site.data.team.members }}
  <li>{{ m.name }} — {{ m.role }}</li>
{{ end }}

Theme overlay. Themes can ship their own data/ directory; site entries win at the file-leaf granularity. If a theme provides data/site.toml and the site provides data/site.toml, the site’s file replaces the theme’s at the .site.data.site key path. The site does not deep-merge fields — to support partial overrides as a theme author, namespace into a subdirectory (data/colors/default.toml + data/colors/dark.toml).

Format notes. TOML at a file root always parses to a map; YAML can parse to a list (data/sponsors.yaml starting with - name: ...) or a scalar, but a map is by far the most useful shape because nested-field access (.site.data.sponsors[0].name) is more readable than positional indexing in most templates.

.site.comments

A pass-through of the [comments] table from site.toml (see Config → [comments]). juicer never ships a comments backend — the config block is opaque to the engine, and theme partials are responsible for reading it and emitting the embed HTML.

provider is the only convention; everything else is provider-specific and theme-defined. A theme that supports several providers typically branches on it:

{{ if .site.comments }}
  {{ if eq .site.comments.provider 'giscus' }}
    {{ partial 'comments/giscus.html' . }}
  {{ end }}
  {{ if eq .site.comments.provider 'utterances' }}
    {{ partial 'comments/utterances.html' . }}
  {{ end }}
  {{ if eq .site.comments.provider 'disqus' }}
    {{ partial 'comments/disqus.html' . }}
  {{ end }}
{{ end }}

The outer {{ if .site.comments }} is the right gate for “comments on or off site-wide” — when no [comments] table is set in site.toml, .site.comments is absent and the conditional evaluates falsy. Themes that allow per-page opt-out also check {{ if .page.comments }} (or similar) so individual posts can override the site-wide setting.

Alias pages

Frontmatter aliases: [...] makes juicer emit a redirect page at each listed URL. The alias layout (or built-in fallback) sees a separate data context:

KeyTypeWhat
.targetStringSite-relative canonical URL
.absTargetStringAbsolute canonical URL (baseURL + .target)
.page.<...>variesEvery field of the canonical page’s record
.site.<...>variesThe full site context

If layouts/_default/alias.html exists, it’s rendered with the data above. Otherwise juicer writes a minimal built-in HTML page with a <meta http-equiv="refresh"> to .target.

.page

The current page’s enriched record:

KeyTypeWhat
.page.titleStringFrontmatter title
.page.summaryStringResolved summary (frontmatter / <!--more--> / fallback)
.page.urlStringSite-relative URL
.page.relPermalinkStringSame as url, named for Hugo parity
.page.permalinkStringAbsolute URL (baseURL + url)
.page.slugStringURL stem (last path segment, no slashes) — useful for in-page anchors when a layout walks .section.pages and wants a stable per-section HTML id. "" for the root index.
.page.isSectionBooleantrue for _index.md pages
.page.parentMap?Enclosing section’s _index record
.page.ancestorsList[Map]Root → parent chain (excluding self)
.page.nextMap?Next page in section by pageOrder
.page.prevMap?Previous page in section
.page.tagsList[String]Frontmatter tags (always a list, even when authored as a single string)
.page.categoriesList[String]Frontmatter categories, normalized the same way
.page.dateOffsetDateTimeParsed publication date (frontmatter date or filesystem mtime fallback)
.page.dateISOString2024-03-12T00:00:00Z — for <time datetime=...>
.page.dateLongStringMarch 12, 2024 — for body copy
.page.dateShortString2024-03-12 — for compact lists
.page.wordCountIntWord count of the rendered HTML body
.page.readingTimeIntEstimated minutes (ceil(wordCount / 200), floor 1 for non-empty pages)
.page.seriesMap?Series block — null when the page is not in a series (see below)
.page.authorMap?First (or only) resolved author registry record, or null
.page.authorsList[Map]All resolved author records — empty list when none
.page.backlinksList[Map]Thin {title, url, summary} records for every page that links to this one — empty list when nothing points here (see below)
.page.assetsList[Map]Page-bundle assets at this page’s section level — {name, url, ext} per file. Empty list when the bundle has no assets (see Page bundles)
.page.<custom>variesAny frontmatter key

.page.series shape

Set when the page declares series: in frontmatter:

KeyTypeWhat
nameStringSeries name as it appeared in frontmatter
pagesList[Map]Every page in the series, ordered (see ordering rules below)
prevMap?Previous page’s record, or null on the first
nextMap?Next page’s record, or null on the last
indexInt1-based position of this page within the series
totalIntNumber of pages in the series

Ordering: explicit seriesOrder ascending first, then .page.date ascending, then filename as a stable tiebreaker.

For section index pages (where .page.isSection is true), additionally:

KeyTypeWhat
.page.pagesList[Map]Non-_index siblings, sorted
.page.subsectionsList[Map]Direct child sections, sorted

A list of thin records — one per page that contains an internal link pointing at the current page’s permalink. Sorted by referrer title for deterministic rendering.

KeyTypeWhat
titleStringReferrer’s frontmatter title (or URL if title absent)
urlStringReferrer’s site-relative URL
summaryStringReferrer’s resolved summary

Templates that need a richer referrer record (tags, date, author, etc.) look it up via .site.pagesByPath[bl.url] — keeping the embedded records thin avoids cycles when two pages link each other.

The inverted index is built during the markdown pass from each page’s AST link destinations. Filtered out at collection time: absolute URLs (https://…, mailto:, tel:), fragment-only anchors (#section), and self-links. Query strings and fragments are stripped before matching, so [X](/target/#section) and [X](/target/?q=1) both count as a link to /target/. Lists, tables, definition lists, footnote definitions, blockquotes, and list items are all walked — anywhere an author can drop a [text](url) in markdown, it counts.

Wiki-style “Linked from” footer in a layout:

{{ if .page.backlinks }}
  <aside class="backlinks">
    <h2>Linked from</h2>
    <ul>
      {{ for bl <- .page.backlinks }}
        <li><a href="{{ bl.url }}">{{ bl.title }}</a>{{ if bl.summary }}{{ bl.summary }}{{ end }}</li>
      {{ end }}
    </ul>
  </aside>
{{ end }}

.section

Always available (for non-_index pages it describes the enclosing section):

KeyTypeWhat
.section.pagesList[Map]Non-_index siblings, sorted
.section.subsectionsList[Map]Direct child sections, sorted
.section.indexMap?Section’s _index record
.section.assetsList[Map]Page-bundle assets for the enclosing section (see Page bundles)
.section.paginatorMapPagination state for the current slice (always present — see below)

.section.paginator

Section index pages render once per slice when paginate is set. Non-section pages get a paginator with total = 1 so the same template can read it unconditionally.

KeyTypeWhat
currentInt1-based index of the current slice
totalIntSlice count
pagesList[Map]The pages on this slice (already sliced — don’t slice again)
firstStringURL of slice 1
lastStringURL of the last slice
prevURLStringURL of the previous slice; empty string on slice 1
nextURLStringURL of the next slice; empty string on the last slice

Slice 2+ lives at <section>/page/<N>/index.html — directory-style URLs that work on any static host without rewrite rules.

Page bundles

A page bundle is a content directory whose non-markdown files (images, attachments, anything that isn’t .md / .toml / .yaml / .yml / .html / .sq) are co-located with the page and get copied to the section’s output URL.

Drop assets next to the markdown:

content/iceland-2024/
  _index.md
  hero.jpg
  skogafoss.jpg
  notes.md

At build time juicer copies the assets to the section’s outdir and exposes them on the page record:

  • hero.jpgdst/html/iceland-2024/hero.jpg → URL /iceland-2024/hero.jpg.
  • {{ .page.assets }} on the section index OR on notes.md is the same list — assets are per-directory, not per-page. .section.assets is an alias for templates that prefer to read them off the section.

Each asset record:

KeyTypeWhat
nameStringOn-disk filename, preserved verbatim (case, dot) — assets are NOT slugified
urlStringSite-relative URL — <section-permalink>/<name>
extStringLowercase extension without the dot, or "" for extensionless files

Iterate in a layout to render a gallery, an attachments list, etc.:

{{ if .page.assets }}
  <ul class="bundle-files">
    {{ for a <- .page.assets }}
      <li><a href="{{ a.url }}">{{ a.name }}</a></li>
    {{ end }}
  </ul>
{{ end }}

Bundle-relative image paths

Inside a bundle, imageVariants, srcset, and imageDims resolve bare (no-leading-/) paths against the page’s source bundle before falling back to static/. That means a hero image lives at the page and the template doesn’t need to know where:

{{ with vs = imageVariants 'hero.jpg' }}
  <picture>
    <source type="image/webp" srcset="{{ srcset 'hero.jpg' 'webp' }}">
    <img src="{{ vs.original }}" width="{{ vs.originalWidth }}" height="{{ vs.originalHeight }}" alt="">
  </picture>
{{ end }}

Move the bundle to a different URL, the layout still works. Absolute paths (/static/hero.jpg) continue to resolve from the source root — the bundle preference only kicks in for bare paths.

When NOT to use a bundle

  • Site-wide chrome (logo, favicon, fonts): keep these in static/ so every page can reach them via an absolute URL.
  • Shared assets used from many pages: same — bundles are for page-specific files. Putting a logo in every bundle would duplicate bytes and break a logo swap.
  • Files in a directory with no markdown content. Bundles need a page to anchor them; orphan assets are silently skipped.

asset builtin

The asset template function resolves a logical name into the URL of its compiled (and optionally fingerprinted) output. The manifest comes from the [assets] pipeline.

<link rel="stylesheet" href="{{ asset 'site.css' }}">
<script src="{{ asset 'main.js' }}" defer></script>
Resolution caseWhat asset 'foo.css' returns
[assets] disabled or no pipeline runfoo.css (the input unchanged)
Pipeline ran, fingerprint = falseThe configured output URL (e.g. /css/foo.css)
Pipeline ran, fingerprint = trueThe fingerprinted URL (e.g. /css/foo.<16-hex>.css)
Name not in manifest (typo / never built)foo.css (the input unchanged) — visible as a broken <link href> in the rendered HTML

The “input unchanged on miss” rule is deliberate: a typo’s effect shows up in the rendered HTML rather than disappearing into an empty string, which is faster to spot in a build output or a browser devtools panel.

Site-wide chrome keys

Top-level site.toml keys that the bundled themes read from .site.* to drive shared chrome — favicon, footer attribution, repo link, custom stylesheets — are catalogued in Theming along with the per-theme [juicerXxx] palette / typography / sizing tables.

Other top-level

KeyTypeWhat
.contentStringRendered markdown body, HTML
.tocMap{ headings: [TocEntry] } — full heading tree
.subListChildren of the first heading, flattened

Page ordering

pageOrder is weight ascending, then name ascending. Pages with no weight frontmatter sort after weighted pages but before any sentinel value.

Note

You’ll almost always want to set explicit weight values on pages that need a particular order — installation before quickstart, etc. Anything that ships a weight lower than the default (9223372036854775807 = Long.MaxValue / 2) wins.

Search

Esc
to navigate to open Esc to close