Helpers
|
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 (master branch, commit 0e38223) and checked on 2026-10-26. The
templates of an Antora UI are Handlebars templates, which are deliberately logic-less: they print values from the UI model but
cannot compute anything on their own. A helper is a small JavaScript function that a template calls to fill that gap, for
example to compare two values, to turn a root-relative URL into a relative one, or to look up another page. This page lists the
helpers that exist, explains the three that resolve content (resolvePage, resolvePageURL and resolveResource), and shows
how to write your own. The running example is a copy of the Default UI that builds the Pocket Units site (a small units
conversion library) used throughout this section. The layouts, partials and the model that helpers read are described in
Templates and UI Model; how a UI project is built and previewed is
described in Building a Custom UI.
Where helpers come from
A template can call helpers from four sources. All of them are called the same way, so a template author rarely needs to know which one is behind a name, but the source decides where the code lives and whether it can be changed.
| Source | Helpers | Where the code lives |
|---|---|---|
Handlebars |
|
Handlebars itself. Antora creates a private Handlebars instance, so nothing you register leaks into other tools. |
Antora ( |
|
Registered by Antora before the UI is consulted, so they exist in every UI, including a UI that ships no helpers at all. |
The UI bundle |
In the Default UI: |
One file per helper in the |
Your UI project |
Any name you choose |
A new file in the same |
Antora registers the built-in helpers first and the helpers of the bundle afterwards, so a helper file in the bundle that has the
same name as a built-in replaces it. The Default UI does exactly this for relativize: it ships src/helpers/relativize.js, a copy that also
accepts the legacy two-argument call, and that copy is the one used when a site is built with the Default UI.
The diagram shows the flow described in the next sections: Antora creates the Handlebars instance, registers its four
built-ins, then every .js file of the bundle’s helpers directory, and finally renders each page. A helper call receives the
arguments written in the template plus a trailing options object that carries the UI model.
… flowchart LR A["Antora starts page composer"] -→ B["Register relativize, resolvePage, resolvePageURL, resolveResource"] B -→ C["Register each helpers/NAME.js of the bundle"] C -→ D["Register partials and compile layouts"] D -→ E["Render each page: layout calls helpers"] E -→ F["Helper receives arguments plus options object"] F -→ G["options.data.root is the UI model"] F -→ H["options.hash holds name=value arguments"] …
Calling a helper
A helper call is a mustache whose first word is the helper name and whose remaining words are arguments. Wrapping the call in
round brackets turns it into a subexpression, whose value can be passed to another helper, tested by if or iterated by each.
Arguments of the form name=value are collected in a hash, which is how the content helpers receive the component, version and
module to resolve against.
{{!-- helper as an expression: its return value is printed (escaped) --}}
Reading time: {{reading-time page.contents}} min.
{{!-- helper as a condition --}}
{{#if (and page.fileUri (not env.CI))}}local file{{/if}}
{{!-- helper as the source of an iteration --}}
{{#each (pages-with-tag 'tutorial')}}{{{./title}}}{{/each}}
{{!-- hash arguments --}}
{{resolvePageURL 'support.adoc' component='about'}}
Double mustaches escape HTML in the result and triple mustaches insert it as is, which matters for values such as page.title
that contain markup. A relativize result is a URL, so the Default UI writes it with triple mustaches. Because round brackets are
how a helper call is nested, a bare helper without arguments, such as {{year}}, is also a call.
Built-in helpers of Antora
relativize
Antora stores the URL of every publishable file as a root-relative path such as /pocket-units/2.0/index.html. The relativize
helper converts such a path into a path relative to the page being rendered, so that the site does not depend on the address it
is served from and can even be browsed from the file system. A value that does not start with / (an absolute URL, a fragment)
is returned unchanged, and an empty value gives #. A fragment on the target is kept. Always wrap a link to something inside the
site in relativize.
{{#with page.next}}
<a href="{{{relativize ./url}}}">{{{./content}}}</a>
{{/with}}
resolvePageURL
resolvePageURL takes a page reference, the same syntax as the target of an xref: macro (see
Resource IDs and xrefs), and returns the root-relative URL of the
page, or nothing when the page does not exist. It is the template counterpart of xref:, usually combined with relativize.
<a href="{{{relativize (resolvePageURL 'about::support.adoc')}}}">Support</a>
<a href="{{{relativize (resolvePageURL 'support.adoc' component='about')}}}">Support</a>
resolvePage
resolvePage resolves the same kind of reference but returns the page itself. By default the result is a UI page model, the same
shape as the page variable, so ./url, ./title and ./attributes are available. It is normally used inside a with
block, which switches the template context to the resolved page and renders nothing when the page is missing.
{{#with (resolvePage 'about::support.adoc')}}<a href="{{{relativize ./url}}}">{{{./title}}}</a>{{/with}}
Passing model=false returns the raw virtual file from the content catalog instead. The raw file exposes pub.url, src,
and the complete set of AsciiDoc document attributes under asciidoc.attributes, not only those prefixed with page-.
{{#with (resolvePage 'program::policy-a.adoc' model=false)}}
<a href="{{relativize ./pub.url}}">{{{./asciidoc.xreftext}}}</a> {{./asciidoc.attributes.next-review-date}}
{{/with}}
How a reference is resolved
Both page helpers resolve a reference against a context built from the page being rendered. The component, version and module of
the current page are the defaults, and a component, version or module written in the reference, or passed as a hash argument,
replaces the matching default. This has a consequence that surprises people when the helper runs in a partial that appears on
every page: a short reference such as index.adoc means index.adoc in the module of the page that is being rendered, so it
works on pages of the ROOT module and silently returns nothing on pages of any other module. A template that is shared by the
whole site should therefore use fully qualified references (ROOT:index.adoc, shared::style-guide.adoc). Resolution never
fails with an error: an unknown page makes the helper return nothing, so check the output rather than expecting a message.
Since Antora 3.2, resolvePage and resolvePageURL can be called without a reference, in which case they resolve the page being
rendered (using its page.relativeSrcPath). Combined with model=false this gives a template access to the raw file of the
current page, which is the way to read AsciiDoc attributes that do not carry the page- prefix. The 3.2 UI model also exposes
that file directly as page.file.
resolveResource
Antora 3.2 added resolveResource, which resolves a reference to any resource, not only pages: images, partials, examples,
attachments. It returns the raw virtual file (never a UI model) or nothing. Use it to test that a resource exists before linking
to it, or to read its src coordinates. An optional publishable argument filters the result: publishable=true accepts only
files that are written to the site (those with an out property, such as pages, images and attachments) and publishable=false
accepts only files that are not (partials and examples), so a mismatch makes the helper return nothing.
{{#with (resolveResource 'ROOT:example$convert.cs')}}Example {{./src.relative}} is in family {{./src.family}}.{{/with}}
{{#with (resolveResource 'ROOT:example$convert.cs' publishable=true)}}published{{else}}not published{{/with}}
A reference without a family resolves as a page, exactly as for resolvePage. The Default UI templates page still states that
there is no helper for resolving a non-page resource, but the 3.2.1 @antora/page-composer package registers resolveResource
next to the other three, and the 3.2 release notes list it, so treat the sentence on the templates page as outdated.
Helpers of the Default UI
The Default UI keeps its helpers in src/helpers. They are plain functions of at most two arguments and are the best template to
copy from. The bundle built from the clone used for this page contains the following.
| Helper | Behaviour | Used in the Default UI for |
|---|---|---|
|
Logical and/or of two or more arguments. Fewer than two arguments throws an error whose text starts with |
Conditions such as the edit-this-page link, and fallback values ( |
|
Logical negation of one value. |
|
|
Strict equality and inequality ( |
Marking the current page and current version in the navigation. |
|
The value plus one, treating a missing value as zero. |
The depth of the navigation tree. |
|
Removes HTML tags from a string; with |
The |
|
The current year as a string. |
Shipped, but not called by the stock templates; useful in a footer. |
|
Replacement for the built-in, which also accepts the legacy |
Every internal link. |
Handlebars also provides log, which prints a value to the console of the process running Antora, formatted as
console.log would. It is the quickest way to inspect the UI model. Running {{log page.url}} in the footer of the example
site printed one line per rendered page, ending in undefined for the 404 page, which has no URL:
/pocket-units/2.0/guides/custom-units.html
/shared/style-guide.html
undefined
Writing a helper
Anatomy
A helper is a CommonJS module in the helpers directory of the UI bundle. Only the file name matters: the name of the file
without its extension becomes the name of the helper, so helpers/reading-time.js is called as reading-time (hyphens are
allowed). Antora registers it automatically; there is no registerHelper call to write. The module must export exactly one
function, whose own name is irrelevant. Files in the directory that do not end in .js are ignored.
Handlebars calls the function with the arguments written in the template followed by one extra argument, an options object. Because the options object always comes last, the number of declared parameters must match the number of arguments the template passes, or the options object moves to an unexpected position. The options object has two properties used in practice:
-
data.root, the complete UI model:site,page,env,siteRootPath,uiRootPath,antoraVersionandcontentCatalog. -
hash, an object with thename=valuearguments of the call.
What the function returns is what the template receives: a boolean for a condition, a string or number for output, or an array for an iteration.
A first helper
The helper below estimates the reading time of the current page. It receives page.contents as its only argument, so the
options object is not needed.
'use strict'
const TAG_RX = /<[^>]+>/g
const WORDS_PER_MINUTE = 200
// {{reading-time page.contents}} -> number of minutes, at least 1
// page.contents is a Buffer, so convert it to a string first
module.exports = (contents) => {
if (!contents) return 0
const words = contents.toString().replace(TAG_RX, ' ').split(/\s+/).filter(Boolean).length
return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE))
}
The first version of this helper called html.replace(...) directly on page.contents and the build stopped with the message
html.replace is not a function in UI template layouts/default.hbs. In Antora 3.2.1 page.contents is a Node.js Buffer, not a
string, so call toString() before using string methods on it. Other model values, such as page.title, are strings.
A helper that reads the content catalog
A helper that needs more than its arguments reads the UI model from data.root. The second helper counts the pages of the
component version being rendered, using contentCatalog, the proxy around Antora’s content catalog that the model exposes for
this purpose.
'use strict'
// {{page-count}} -> number of pages in the component version being rendered
module.exports = ({ data: { root } }) => {
const { contentCatalog, page } = root
if (!page.component) return 0
const { name } = page.component
const { version } = page
return contentCatalog.getPages().filter(({ src }) => src.component === name && src.version === version).length
}
The proxy exposes findBy, getAll, getById, getComponent, getComponentVersion, getComponents,
getComponentsSortedBy, getFiles, getPages, getSiteStartPage, resolvePage and resolveResource. The Antora
documentation says it should be used only from a UI helper. Virtual files returned by the catalog are not UI page models; to get
the model of each file, run it through buildPageUiModel from @antora/page-composer. The third helper lists the pages whose
page-tags attribute contains a given tag.
'use strict'
// {{#each (pages-with-tag 'tutorial')}} -> UI models of publishable pages whose page-tags attribute lists the tag
module.exports = (tag, { data }) => {
const { contentCatalog, site } = data.root
const pages = contentCatalog.getPages(({ asciidoc, out }) => {
if (!out || !asciidoc) return
const pageTags = asciidoc.attributes['page-tags']
return pageTags && pageTags.split(', ').includes(tag)
})
const { buildPageUiModel } = module.parent.require('@antora/page-composer/build-ui-model')
return pages.map((page) => buildPageUiModel(site, page, contentCatalog))
}
The check on out skips files that are not published (partials, examples), and the check on asciidoc skips files that have
no AsciiDoc document. Two of the three Pocket Units pages were given an attribute (:page-tags: tutorial and
:page-tags: tutorial, units) and the helpers were used from the footer partial:
<footer class="footer">
<p>This page was built using the Antora default UI.</p>
<p>The source code for this UI is licensed under the terms of the MPL-2.0 license.</p>
{{#if page.component}}
<p class="helper-demo">
Reading time: {{reading-time page.contents}} min.
Pages in this version: {{page-count}}.
{{#with (resolvePage 'ROOT:index.adoc')}}Start page: <a href="{{{relativize ./url}}}">{{{./title}}}</a>.{{/with}}
Link only: <a href="{{{relativize (resolvePageURL 'guides:custom-units.adoc')}}}">custom units</a>.
</p>
<ul class="tutorials">
{{#each (pages-with-tag 'tutorial')}}
<li><a href="{{{relativize ./url}}}">{{{./title}}}</a> ({{./attributes.tags}})</li>
{{/each}}
</ul>
{{/if}}
</footer>
npx gulp lint accepted the three files, npx gulp bundle produced the bundle, and npx antora antora-playbook.yml built the
site. The footer of guides/converting-lengths.html in the generated site contained:
<p class="helper-demo">
Reading time: 1 min.
Pages in this version: 5.
Start page: <a href="../index.html">Pocket Units 2.0</a>.
Link only: <a href="custom-units.html">custom units</a>.
</p>
<ul class="tutorials">
<li><a href="../getting-started.html">Getting Started</a> (tutorial)</li>
<li><a href="converting-lengths.html">Converting Lengths</a> (tutorial, units)</li>
</ul>
Because the footer partial is rendered for every page, the helpers run once per page. Keep their work small, or compute the result once in an Antora extension (see Antora Extensions) and read it from a page attribute; a helper that scans the whole catalog on a site with thousands of pages slows down the build.
Module context, require and registration order
Antora loads each helper file with a small loader instead of Node’s own require, and gives it a module object whose
filename is a path inside a pseudo directory named ui-helpers in the playbook directory (printing module.id in a helper of
the example site gave .../site-helpers/ui-helpers/where.js). Two consequences follow:
-
requireresolves packages from the playbook project, so a helper can use any package that is a dependency of the project that runs Antora, but a UI bundle cannot bring its ownnode_modules. -
module.parentis the@antora/page-composermodule, somodule.parent.require('@antora/page-composer/build-ui-model')reaches the Antora internals as in the helper above. The Antora documentation also showsrequire.main.require(...)for the same purpose; in the example siterequire.mainwas an object as well, butmodule.parent.requireis the form that does not depend on how the process was started.
Hash arguments and several parameters
A helper that takes a variable number of arguments reads them from the rest parameters and treats the last one as the options
object, as the and and or helpers of the Default UI do. Hash arguments are in options.hash. A short helper that joins an
array with a separator given either positionally or as a hash argument looks like this.
'use strict'
// {{join list ', '}} and {{join list separator=', '}}
module.exports = (list, ...rest) => {
const options = rest.pop()
const separator = typeof rest[0] === 'string' ? rest[0] : options.hash.separator || ', '
return Array.isArray(list) ? list.join(separator) : ''
}
This helper was added to a supplemental UI directory (ui.supplemental_files) of the example site and was picked up without any
other change; a README.md placed in the same helpers directory was ignored. Called with an array of page objects it joined
their string forms ([object Object], [object Object]), and called with a value that is not an array it returned an empty string.
Debugging and common errors
Antora reports a template or helper problem as a fatal error that names the template, and the build stops. Messages seen while preparing this page:
| Message | Cause |
|---|---|
|
The template calls a helper that does not exist, for example because the file is missing from |
|
A Default UI helper rejected its arguments; the message comes from the helper, so your own helpers can throw useful messages too. |
|
A helper treated |
When a helper returns nothing, the template renders nothing and no message is printed, which is the case for an unresolved
reference and for resolvePage or resolveResource used inside a with block. Use {{log}} on the value of the call, or an
else branch ({{#with (resolvePage 'ROOT:no-such-page.adoc')}}Found.{{else}}Missing page renders nothing.{{/with}} rendered the
else text in the example site), to see what the helper returned.
Helpers in the preview
The npx gulp preview and preview:build tasks of the Default UI project render the templates against the sample model in
preview-src/ui-model.yml, outside of Antora. They register every file of src/helpers, but of the Antora built-ins they
provide only simplified resolvePage and resolvePageURL stubs, which turn a reference into a plausible URL without a content
catalog. There is no resolveResource and no contentCatalog. In the example project, a template calling resolveResource
failed the preview with Missing helper: "resolveResource" in UI template src/partials/footer-content.hbs, and a helper that
used contentCatalog failed with Cannot read properties of undefined (reading 'getPages') in UI template
src/partials/footer-content.hbs. Either guard the helper (if (!contentCatalog) return) or test it against a real site by
building the bundle and pointing a playbook at it, as described in Building a Custom UI.
Pitfalls and version notes
-
Helpers cannot be added to a site without changing the UI. If you do not want to fork the Default UI, add the file to the supplemental UI (see Supplemental UI) under
helpers/. -
A UI bundle is cached by URL. After changing a helper and publishing the bundle at the same URL, build with
--fetchandsnapshot: true, or clear the cache, to be sure the new helper is used. -
resolvePageandresolvePageURLwithout a reference,resolveResource,page.fileand the dynamicfilesproperty of a component version were introduced in Antora 3.2.0. A bundle that uses them needs Antora 3.2 or newer. -
A short reference resolves in the module of the page being rendered (see How a reference is resolved). Use qualified references in shared partials.
-
Helpers with side effects, such as writing files, are not what helpers are for. Use an extension instead.