Templates and UI Model

This content was generated with the assistance of AI and should be verified against the official documentation of each tool before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against Antora 3.2.1 and the Antora Default UI (repository checkout 0e38223, which has no release tags), and checked on 2026-10-26. An Antora UI bundle contains Handlebars templates that wrap the HTML Antora produces from each AsciiDoc page in a complete web page. This page explains the two kinds of template (layouts and partials), the model of data they can read, how a page chooses its layout, and how to add a layout of your own. Writing helpers, styling and packaging are covered on the sibling pages listed in the references of the UI overview.

A layout combines the UI model and the page contents with partials to produce one HTML page

The figure shows the flow described below: Antora builds the UI model and the converted page contents, picks a layout by page.layout, the layout pulls in partials, and the result is one standalone HTML file per page.

What templates do and why they exist

Antora converts every AsciiDoc page to an embeddable HTML fragment (the article body). A fragment is not yet a web page: it has no head, no navigation, no footer. The UI bundle supplies that frame. Keeping the frame in templates, separate from the content, means the same documentation can be re-skinned by swapping one bundle (see Building a custom UI) without touching a single page.

Antora uses Handlebars for templates. Handlebars templates are "logic-less": they are HTML with mustache tags that insert values from a model. Some conditionals and loops exist ({{#if}}, {{#each}}, {{#with}}), but real computation belongs in helpers (see Helpers).

The template syntax in short:

  • {{page.title}} inserts a value and escapes it for HTML. {{{page.title}}} (triple mustaches) inserts it as is. Use triple mustaches for values that already contain HTML, such as page.title and page.contents, and for URLs.

  • {{> head}} invokes the partial named head and inserts its output. It is not a model reference.

  • {{#if page.home}}, {{#with page.next}} and {{#each site.components}} open block helpers that end with a matching closing tag. Inside {{#with}} and {{#each}} the current item is this, and @root still reaches the top of the model.

  • A mustache preceded by a backslash is disabled, so a backslash right before the opening mustaches silently breaks it. Use forward slashes.

  • {{!-- comment --}} is a template comment that never reaches the output.

The two kinds of template

The bundle loader sorts every file by its first path segment. A file under layouts/ is a layout, a file under partials/ is a partial, a file under helpers/ is a helper, and everything else is a static asset published under the UI output directory (default _). The template name is the file name without its extension (the stem), so layouts/default.hbs is the layout default and partials/head.hbs is the partial head. Partials are registered by stem only, so two partials in different sub-directories with the same file name would collide.

Layouts

A layout is the root template of a page: it produces the whole HTML document. Antora compiles each layout once and runs it for every page that selects it. The Default UI has two layouts:

layouts/default.hbs
<!DOCTYPE html>
<html lang="en">
  <head>
{{> head defaultPageTitle='Untitled'}}
  </head>
  <body class="article{{#with (or page.attributes.role page.role)}} {{{this}}}{{/with}}">
{{> header}}
{{> body}}
{{> footer}}
  </body>
</html>

The layout is short on purpose. It sets the document skeleton, passes defaultPageTitle into the head partial as a hash argument, adds the page role as a CSS class on <body>, and delegates every region to a partial. The 404 layout is nearly identical, with defaultPageTitle='Page Not Found' and <body class="status-404">.

Partials

A partial is a reusable fragment included by a layout or another partial. The Default UI splits a page into regions, each in its own file, which keeps each file small enough to override on its own (see Supplemental UI). The partials and what they render:

Partial Purpose

head

Calls, in order, head-prelude, head-title, head-info, head-styles, head-meta, head-scripts and head-icons.

head-prelude

Empty hook placed at the start of <head>.

head-title

The <title> element: the detagged page title, followed by the site title.

head-info

Charset and viewport meta tags, canonical link, previous and next links, description, keywords and the generator meta tag.

head-styles

The link to css/site.css through uiRootPath.

head-meta, head-icons

Empty hooks for extra meta tags and favicons.

head-scripts

Hook for scripts in <head> (the Default UI ships it empty).

header

Calls header-scripts and header-content, then renders the top navbar, including the search input when search is on.

header-scripts, header-content

Hooks for the start of the page. header-scripts carries a Google Analytics snippet driven by site.keys.googleAnalytics.

body

The page body: the navigation (nav) and the main region (main).

nav, nav-menu, nav-explore, nav-tree, nav-toggle

The left navigation panels, the component explorer and the toggle button.

main

The main region: toolbar, then either the 404 article or toc and article.

toolbar, breadcrumbs, page-versions, edit-this-page

The strip above the article: nav toggle, home link, breadcrumbs, version menu and the "Edit this Page" link.

article, article-404

The <article>: page title, page.contents and pagination, or the not-found message.

toc, pagination

The right-hand contents sidebar and the previous and next links.

footer

Calls footer-content and footer-scripts.

footer-content

The footer markup.

footer-scripts

The site script, the highlight script, and search-scripts when a search provider is set.

Many of these are deliberate empty hooks (head-prelude, head-meta, head-scripts, header-content). A site that needs extra markup overrides one hook instead of copying a large partial. A partial is invoked with the same context as its caller, so page, site and the other variables are visible everywhere, and a hash argument such as defaultPageTitle becomes a variable inside the partial.

Two partials matter for the integrations described on other pages. footer-scripts includes a partial named search-scripts only when env.SITE_SEARCH_PROVIDER is set, and the Default UI does not contain a search-scripts file of its own: a search extension supplies it (see Search and analytics integration). Invoking a partial that does not exist is a build error, as shown under "Errors and diagnostics".

Which files are required

A UI bundle with a custom set of templates needs very little. Antora requires exactly one layout for a page to render: the default layout (layouts/default.hbs, or the layout named by ui.default_layout). The 404 layout is required only when the playbook sets site.url, because Antora builds 404.html only then. Both facts were checked by removing the files from an unzipped bundle and running Antora 3.2.1; the results are under "Errors and diagnostics". Partials and helpers are optional: a layout can be a single self-contained file.

The UI model

Every layout and partial reads from one object, the UI model. Antora builds it for each page: a base part shared by all pages, plus a page object and two path variables computed per page.

Top-level variables

Variable Description

site

Site-wide information, described below.

page

Information about the page being composed, described below.

env

The environment variables of the Antora process (process.env). Use it to switch behaviour from the outside, for example env.SITE_SEARCH_PROVIDER or env.CI.

siteRootPath

The relative path from the current page to the root of the published site. In the 404 page it is root-relative instead, so the page keeps working at any depth.

uiRootPath

The relative path from the current page to the UI output directory (_ by default), used to link CSS, scripts and images. Root-relative in the 404 page.

antoraVersion

The version of Antora that built the site, for example 3.2.1.

contentCatalog

A proxy over Antora’s content catalog (components, versions, pages and resources). The official page states it should only be used from a UI helper, never directly from a template.

The value of uiRootPath is easy to check. In the scratch site built for this page, landing.html printed ui-root=../_ site-root=.. because the page sits one directory below the site root.

The site object

Property Description

site.title

The site title from site.title in the playbook.

site.url

The base URL from the playbook, when set. It includes the site path.

site.path

The path part of site.url (for example /docs), empty when there is none.

site.homeUrl

The URL of the start page, when a start page is defined.

site.components

A map of every component, keyed by component name and sorted by title. Each has its versions and their navigation.

site.keys

A copy of the playbook site.keys map, used for values such as a Google Analytics id.

site.ui.url

The root-relative URL of the UI output directory.

site.ui.defaultLayout

The default layout name, default unless ui.default_layout is set.

The page object

Property Description

page.title

The page title as HTML (may contain inline elements and character references, hence triple mustaches).

page.contents

The converted article as an HTML fragment.

page.layout

The layout name for this page: the page-layout attribute, else site.ui.defaultLayout. The 404 page is always 404.

page.attributes

Every AsciiDoc attribute starting with page-, with that prefix removed. :page-status: is page.attributes.status.

page.description, page.keywords, page.author

The matching header attributes of the page.

page.role

The role attribute of the document, when set.

page.url, page.canonicalUrl

The root-relative URL, and the absolute canonical URL (only when site.url is absolute and a non-prerelease version exists).

page.component, page.componentVersion, page.module

The component, its version record, and the module name. page.version and page.displayVersion give the version.

page.versions, page.latest

All versions of this page across component versions, present only when the component has more than one.

page.navigation, page.breadcrumbs, page.parent, page.previous, page.next

The navigation tree of the component version and the position of the page in it.

page.home

True when this is the start page of the site.

page.relativeSrcPath, page.origin

The page path relative to its module pages directory and the content source it came from.

page.editUrl, page.fileUri

The web editor URL and, for a local worktree, the file:// URI.

page.file

The underlying virtual file of the page, new in Antora 3.2.

The release notes for Antora 3.2 list page.file as a new UI model property, while the Default UI templates page does not document it (it documents page.fileUri). It is the virtual file Antora keeps for the page, with the original source path under file.src, the published URL under file.pub and the AsciiDoc document under file.asciidoc. Printing it from a layout with {{log page.file.src}} produced, in the scratch site:

{
  path: 'modules/ROOT/pages/landing.adoc',
  basename: 'landing.adoc',
  stem: 'landing',
  extname: '.adoc',
  origin: { type: 'git', reftype: 'branch', refname: 'main', branch: 'main', startPath: 'docs', ... },
  family: 'page',
  relative: 'landing.adoc',
  module: 'ROOT',
  moduleRootPath: '..',
  component: 'demo',
  version: '',
  mediaType: 'text/asciidoc'
}

The listing is shortened: the real output also contained absolute paths and URIs, which are omitted here. Source: Antora docs: What’s new in Antora 3.2

The model is described by the Default UI documentation as not final, so treat properties that only appear in the source (like page.file internals) as less stable than the documented ones. To see exactly what a template can read, print it: {{log page}} writes the object to the console in the same format as Node.js console.log.

Selecting a layout

A page chooses its layout through the page-layout attribute, and a site chooses a default through the playbook:

  • :page-layout: name in the page header sets page.layout to name for that page. Antora looks up layouts/name.hbs.

  • ui.default_layout in the playbook sets the layout used by every page that does not set page-layout. Without it the default is default.

  • The 404 page ignores both and always uses layouts/404.hbs.

Resolution follows a fixed order, implemented in the page composer:

flowchart TD A["page.layout = page-layout attribute or ui.default_layout"] --> B{"layout file exists?"} B -- yes --> C["render layouts/name.hbs"] B -- no --> D{"is the layout the default one?"} D -- yes --> E["fatal: default layout not found"] D -- no --> F{"default layout exists?"} F -- yes --> G["warn and render the default layout"] F -- no --> H["fatal: Neither name layout or fallback default layout found"]

An unknown page-layout is therefore only a warning, not an error, and the page silently looks like the rest of the site. The warning is the only hint, so read the build log.

Worked example: a layout for Pocket Units reference pages

The running example is Pocket Units, a small units-conversion library. Its reference pages should show a status banner above the article, driven by a page attribute, while every other page keeps the standard look. The component, scratch Git repository and unzipped Default UI bundle used here were built on 2026-10-26 with Antora 3.2.1.

The page picks the new layout and supplies the banner text. The attribute page-status becomes page.attributes.status:

docs/modules/ROOT/pages/convert.adoc
= Convert units
:page-layout: reference
:page-status: preview

Convert a value from one unit to another.

The layout is a copy of default.hbs with the body partial inlined, so the banner can be placed inside the content region. It reuses every other partial:

layouts/reference.hbs
<!DOCTYPE html>
<html lang="en">
  <head>
{{> head defaultPageTitle='Untitled'}}
  </head>
  <body class="article{{#with (or page.attributes.role page.role)}} {{{this}}}{{/with}}">
{{> header}}
<div class="body">
{{> nav}}
<main class="article">
{{> toolbar}}
  <div class="content">
{{#with page.attributes.status}}
    <p class="status-banner">Status: {{this}}</p>
{{/with}}
{{> toc}}
{{> article}}
  </div>
</main>
</div>
{{> footer}}
  </body>
</html>

Running npx antora playbook.yml with a local, extracted bundle directory (ui.bundle.url accepts one) built the site without warnings. The generated convert.html contains the banner, and the standard pages still use default.hbs:

$ grep -n "status-banner" out/demo/convert.html
102:    <p class="status-banner">Status: preview</p>
$ grep -n "<body" out/demo/index.html out/404.html
out/demo/index.html:11:  <body class="article home">
out/404.html:10:  <body class="status-404">

The banner needs a style rule, which belongs in the UI stylesheet (see Stylesheets, fonts and static files). A site that wants a layout for every page sets it once in the playbook:

ui:
  bundle:
    url: ./ui-bundle.zip
  default_layout: reference

The default_layout: landing variant of this was run against a bundle with a landing.hbs layout and every page, including the start page, used it; page.layout printed landing and site.ui.defaultLayout printed landing.

Errors and diagnostics

These messages were produced by Antora 3.2.1 against an unzipped Default UI bundle that was modified as described.

Situation Result

layouts/404.hbs removed and site.url set

Fatal: 404 layout not found.

layouts/404.hbs removed and no site.url

Build succeeds. No 404.html is written, so the file is not needed.

layouts/default.hbs removed

Fatal: default layout not found.

:page-layout: landing but no landing.hbs

Warning: Page layout specified by page not found: landing (reverting to default layout), followed by the page file.

A layout includes {{> nofooter}} that does not exist

Fatal: The partial nofooter could not be found in UI template layouts/landing.hbs.

Handlebars runtime error inside a partial

Fatal. According to the page composer source, the message ends with the template path, in the form in UI template partials/ followed by the partial name and .hbs. This case was not executed here.

Add --stacktrace to the Antora command to see the full cause of a fatal error.

Page attributes the Default UI reads

The layouts above read page.attributes, and the Default UI uses a handful of attributes. They are documented on the template customization page, and they are the first things to check before writing a new layout:

Attribute Effect in the Default UI

page-role

Space-separated classes added to <body>. The reserved role -toc removes the contents sidebar.

page-pagination

Enables previous and next links, set in the playbook under asciidoc.attributes. A value of @ lets a page turn it off.

page-toctitle

Heading of the contents sidebar (default Contents).

page-toclevels

Heading depth shown in the sidebar (default 2).

Pitfalls and version notes

  • A remote bundle is cached, so a changed template is not seen until the bundle is fetched again. The caching rules and snapshot: true are in Publishing a UI bundle.

  • contentCatalog is only meant for helpers. If a layout needs data from another page, such as its title, resolve it with a helper like resolvePage, which is covered on the Helpers page.

  • Use relativize for every internal link and uiRootPath for every UI asset so a built site works from any directory or from a file system.

  • A template error points to the template, not to a line in your source: the partial name is derived from the failing call.

  • A layout that forgets the search-input or search-scripts wiring loses search silently. Keep the pieces from header.hbs and footer-scripts.hbs when you copy a layout.

  • The model documentation says the model may change. page.file is new in 3.2, so a UI that reads it will not work on older Antora versions.

  • Layout names are file stems: layouts/Reference.hbs and :page-layout: reference do not match on case-sensitive file systems. Keep layout names lowercase.