UI Bundle

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 at commit 0e38223a of its default branch, and checked on 2026-10-26. Antora produces the content of a documentation site (the HTML of every page body, the navigation tree, the breadcrumbs), but it does not decide what the site looks like. Colours, fonts, the header, the sidebar and the page template come from a separate artifact, the UI bundle, which the playbook points to. This page explains what that artifact is and how Antora consumes it. The pages that follow cover building one, writing templates and helpers, styling, supplementing and publishing.

What a UI bundle is

A UI bundle is a ZIP archive (or, for local use, a directory with the same layout as the extracted archive) that holds everything needed to turn Antora’s page model into a themed site. The Antora documentation describes it as an archive "that contains one or more UIs for a site". It has two kinds of content:

  • Template and configuration files that Antora reads while it generates the HTML. They are never copied to the site.

  • Static files (stylesheets, scripts, images, fonts and anything else) that Antora copies to the published site as they are.

The table lists the top-level entries of the bundle.

Entry Purpose Ends up in the site?

layouts/

Page layouts written in Handlebars (.hbs). Each layout is a full HTML document. default.hbs is the layout for pages and 404.hbs is the layout of the not-found page.

No. Used to compose every HTML page.

partials/

Reusable Handlebars fragments (header, nav, toc, footer, …​) included from layouts and from other partials with {{> name}}.

No. Used to compose every HTML page.

helpers/

JavaScript functions, one per file, that templates call as Handlebars helpers (for example eq, year, relativize).

No. Executed during the build.

ui.yml

Optional UI descriptor. Its static_files key lists files to promote to the site root.

No. Read as configuration.

css/, js/, img/, font/

The stylesheet, the browser scripts, the images and the web fonts used by the layouts.

Yes, under the UI output directory (_ by default).

Any other file or folder

Classified as static by Antora, for example a favicon.ico or an about/ folder.

Yes, under the UI output directory, unless ui.yml promotes it to the site root.

The official Default UI documentation states this classification rule: the folders helpers, layouts and partials and the optional ui.yml are the files Antora recognizes as non-publishable, and every other file or folder is static. Files and folders whose names begin with a dot are ignored unless ui.yml lists them explicitly.

Anatomy of a UI bundle and where each part ends up in the published site

The figure shows the flow: the three template folders and the UI model produce the HTML pages, the four asset folders are copied under _, and the files listed in ui.yml are promoted to the site root.

The only required files

The only required file in a bundle is the default page layout, layouts/default.hbs. The documentation adds that, when the 404 page is enabled, layouts/404.hbs is required as well. Both statements were tried against a copy of the Default UI bundle with a Pocket Units site whose playbook sets site.url. Removing layouts/default.hbs stopped the build with this fatal error:

{"level":"fatal","name":"antora","hint":"Add the --stacktrace option to see the cause of the error.","msg":"default layout not found"}

Removing layouts/404.hbs instead stopped the build with the message 404 layout not found. Everything else in a bundle is optional: a bundle with only layouts/default.hbs and layouts/404.hbs is a valid, if unstyled, UI.

How Antora uses the bundle

During a run Antora loads the bundle after reading the playbook. It extracts the files into a virtual catalog, overlays the supplemental UI on top (see Supplemental UI), copies the static files into the output, and then, for every page, builds a UI model and renders the chosen layout with it (see Templates and UI Model). The page content and the navigation come from the content sources, so the same bundle can be reused across any number of sites.

The ui key of the playbook controls all of this. The full playbook schema is described in Playbook; the keys that matter for the bundle are these:

Key Meaning Default

ui.bundle.url

Required. A URL or a filesystem path of the bundle. A remote value must be a ZIP archive. A local value may be a ZIP archive or a directory.

None. A playbook without it fails validation.

ui.bundle.snapshot

Marks a remote bundle as a snapshot: Antora downloads it again whenever fetching is enabled, even though the URL did not change.

false

ui.bundle.start_path

A path inside the bundle where Antora starts reading files. Useful when one archive packages several UIs.

The root of the bundle

ui.output_dir

Where the UI files are published, relative to the root of the published site.

_

ui.default_layout

The stem (file name without extension) of the layout Antora applies to pages that do not set the page-layout attribute.

default

ui.supplemental_files

A directory or a list of virtual files laid over the bundle.

None

Loading a bundle by URL

The smallest working ui configuration is the bundle.url key. This is the form the Antora documentation uses for its own reference bundle, with snapshot: true because that URL never changes while the file behind it does:

antora-playbook.yml
site:
  title: Pocket Units Docs
  url: https://docs.example.org
  start_page: pocket-units::index.adoc
content:
  sources:
  - url: https://github.com/example/pocket-units.git
    branches: [v1.0, v2.0]
ui:
  bundle:
    url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
    snapshot: true

This example was not executed here because it downloads the bundle from the network and clones a repository that does not exist. The same playbook with a local bundle path instead of the URL is used in the runs shown below.

Loading a local bundle

When url is a filesystem path, Antora reads that file or directory directly. A path may point to a ZIP archive, to a directory where the archive was extracted, or to the directory the Default UI build writes before it zips it. A relative path follows the usual playbook rule: a leading ~ is the home directory, a leading . is relative to the location of the playbook file, and anything else (including a leading ~+) is relative to the current working directory.

ui:
  bundle:
    url: ./../docs-ui/build/ui-bundle.zip

A missing path is reported before anything else happens. Running a playbook whose ui.bundle.url was ./nope.zip printed:

{"level":"fatal","name":"antora","hint":"Add the --stacktrace option to see the cause of the error.","msg":"Specified UI bundle does not exist: <path to the playbook directory>/nope.zip (resolved from url: ./nope.zip)"}

A playbook with no ui.bundle.url at all is rejected by the schema with ui.bundle.url: must be of type String.

Loading the bundle from a directory is a good way to debug templates, because the directory can be rebuilt without zipping it each time. That workflow is covered in Building a Custom UI.

Caching: by URL, not by content

Antora caches a remote bundle the first time it downloads it. The cache key is the signature of the URL. On later runs Antora loads the cached copy as long as the url value is unchanged, so a site can be generated offline. The cache lives in Antora’s cache directory, which defaults to $HOME/Library/Caches/antora on macOS, $XDG_CACHE_HOME/antora or $HOME/.cache/antora on Linux and $APPDATA/antora/Caches on Windows. On the machine used for this page it contained a ui folder with one ZIP file named after a hash of the URL, next to the content and collector folders. The location can be changed with runtime.cache_dir.

The rule that follows is the one that surprises people most. Antora does not look at the bytes of the archive. If the URL stays the same and the archive behind it changes, Antora keeps using the old cached copy, even when --fetch is passed, unless the bundle is marked as a snapshot. That is what snapshot is for:

  • snapshot: false (default): the bundle is downloaded once per distinct URL. To get a new version, change the URL (for example a versioned file name such as ui-bundle-1.4.0.zip), delete the cache, or switch to another cache directory.

  • snapshot: true: the bundle is downloaded again every time fetching is enabled, with --fetch on the command line or runtime.fetch: true in the playbook. Without fetching, even a snapshot comes from the cache.

runtime:
  fetch: true

The documentation states that snapshot is only needed for a remote bundle, because a bundle read from the filesystem is always read again from the file named. The playbook of this documentation site sets snapshot: true on a relative local path (ui-bundle.zip), which therefore does no harm but, according to the documentation, has no effect either. See this site as a case study.

Selecting a UI with start_path

start_path makes Antora ignore every file outside one folder of the archive. It is intended for a single archive that packages several UIs (a light theme and a dark theme, for instance). Only the files below the selected folder form the UI, so the layouts, partials, helpers and static files all have to be inside it.

Choosing where the UI files go with output_dir

The UI’s static files are published into a folder named _, relative to the root of the site. output_dir replaces that folder. Antora recomputes every reference to the UI files through the uiRootPath placeholder that the templates use, so no template needs to change when the folder moves.

ui:
  bundle:
    url: ./themes.zip
    start_path: dark
  output_dir: _/dark

Choosing a layout with default_layout

Every page is rendered with a layout. The page attribute page-layout selects one by name. A page that does not set it gets the default layout, which is layouts/default.hbs unless ui.default_layout names another stem. For example default_layout: article makes layouts/article.hbs the layout of every page that does not set page-layout.

ui:
  bundle:
    url: ./../docs-ui/build/ui-bundle.zip
  default_layout: article

The layout file must exist in the bundle. Pointing default_layout at a layout that is not there stops the build, as this run (with default_layout: article and the Default UI, which has no article.hbs) showed:

{"level":"fatal","name":"antora","hint":"Add the --stacktrace option to see the cause of the error.","msg":"article layout not found"}

A worked run: a bundle with two themes

The following run uses the Pocket Units sources. The Default UI was built with npx gulp bundle, the extracted result was placed in a dark/ folder, and the folder was zipped into themes.zip. The command unzip -l themes.zip starts with the dark/ folder, which is why start_path: dark selects it. The playbook is the one from the output_dir example above, plus the content source:

antora-playbook.yml
site:
  title: Pocket Units Docs
  url: http://localhost:5252
  start_page: pocket-units::index.adoc
content:
  sources:
  - url: ../antora/src/pocket-units
    branches: [v1.0, v2.0]
  - url: ../antora/src/shared
    branches: main
ui:
  bundle:
    url: ./themes.zip
    start_path: dark
  output_dir: _/dark
output:
  dir: build/site
npx antora antora-playbook.yml
ls build/site build/site/_ build/site/_/dark
grep -o '<link rel="stylesheet"[^>]*>' build/site/pocket-units/2.0/index.html

The build finished without errors. The listing shows that the UI files landed under _/dark, and that the page found them through a relative uiRootPath:

build/site:
404.html
_
index.html
pocket-units
shared
sitemap-pocket-units.xml
sitemap-shared.xml
sitemap.xml

build/site/_:
dark

build/site/_/dark:
css
font
img
js

<link rel="stylesheet" href="../../_/dark/css/site.css">

Neither layouts/, partials/ nor helpers/ appears in the site. A second run used a directory instead of a ZIP and added a ui.yml with static_files: [.nojekyll] plus an empty .nojekyll file in the bundle. The site root then contained .nojekyll, 404.html, _, index.html, pocket-units and sitemap.xml, and _ contained only css, font, img and js. That is the promotion rule at work: the hidden file was moved out of the UI folder to the site root.

The ui.yml descriptor

ui.yml is the UI descriptor and must sit at the root of the bundle (of the folder selected by start_path). Its one documented key is static_files, an array whose entries are exact relative paths or globs of paths in the bundle. Each entry must match files, not folders. Matched files are copied to the site root instead of the UI output directory. Wildcards never match hidden files, so a dot file such as .nojekyll must be listed by its exact path.

ui.yml
static_files:
- favicon.ico
- .nojekyll

In a UI project the file is kept as src/ui.yml, and the build copies it to the root of the bundle. The Default UI does not ship one. Its output has none, and in the bundle built for this page a ui.yml was not present. This documentation site’s playbook supplies the descriptor through ui.supplemental_files instead; that technique, including the rule that a supplemental ui.yml replaces the one in the bundle, is described in Supplemental UI. Static files and promotion in detail are covered in Stylesheets, Fonts and Static Files.

The Antora Default UI

Antora provides a reference bundle, the Default UI, from the repository https://gitlab.com/antora/antora-ui-default. It is the UI that most Antora sites start from, it is licensed under MPL-2.0, and its documentation says it contains the UI assets (page templates, CSS, JavaScript, images and so on) plus a Gulp build script that can preview the UI locally with live updates or package it for Antora. At the time of this check the repository had no release tags. The default branch is named master, and the bundle is distributed as the artifact of a GitLab CI job (bundle-stable), which publishes build/ui-bundle.zip. That is why the playbook example above uses a jobs/artifacts/HEAD/raw/... URL with snapshot: true: the file behind the URL changes with every build of the default branch.

Two terms from its documentation are worth separating:

  • The UI project is the Git repository: sources, the Gulp build, package.json and everything needed to produce the bundle. It is the development workspace.

  • The UI bundle is the distributable archive produced from it: compiled and consolidated files ready for Antora.

Building the Default UI in a scratch clone (npm ci followed by npx gulp bundle) produced a build/ui-bundle.zip of about 307 kB with 74 entries. The source folders of the project map to bundle folders like this:

UI project Bundle Notes

src/layouts/ (default.hbs, 404.hbs)

layouts/

The only layouts in the Default UI.

src/partials/ (29 files)

partials/

head, header, body, nav, toolbar, article, toc, pagination, footer, and their parts.

src/helpers/ (9 files)

helpers/

and, detag, eq, increment, ne, not, or, relativize, year.

src/css/

css/site.css

Many source stylesheets are concatenated into one file by the build.

src/js/

js/site.js and js/vendor/

js/vendor/highlight.js is generated during the build.

src/img/

img/

Icons used by the layouts.

Fonts from npm packages

font/

Generated during the build; 17 font files in this build.

The Default UI is a starting point rather than a finished brand. Teams typically either use it as is with a few supplemental files, or fork the project into their own UI repository and publish their own bundle, which is the subject of the next pages:

Pitfalls and version notes

  • Changing a remote bundle without changing its URL does nothing unless snapshot: true is set and fetching is enabled (--fetch or runtime.fetch). Versioned URLs avoid the problem, at the price of editing the playbook for each release.

  • A remote url must point to a ZIP. A directory is accepted only as a local path.

  • start_path must name the folder that contains layouts/, partials/ and so on. Files outside it are ignored.

  • A missing layout is fatal. The messages seen while preparing this page were default layout not found, 404 layout not found and article layout not found (an unknown default_layout).

  • output_dir is relative to the site root, and output.dir is the site folder itself. UI files for output_dir: _/dark and output.dir: ./public end up in public/_/dark.

  • Antora 3.2 added page.file to the UI model, a resolveResource helper and an argument-less form of the resolvePage and resolvePageURL helpers. A bundle that uses them needs Antora 3.2 or newer.

  • The Antora project documents the Default UI as the reference bundle. Whether a custom bundle is published as a CI artifact, a release asset or a committed file is a choice of the project, discussed in Publishing a UI Bundle.