Publishing

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 checked on 2026-10-26. Antora produces a folder of static files (build/site by default) and does not publish it: that is the job of a hosting service and a CI job. The three services below cover most cases. What differs between them is who serves the files, whether Jekyll touches them, and how page redirects are expressed. The CLI options used in the examples are explained in CLI, Docker, cache and private repositories.

flowchart LR A["antora antora-playbook.yml"] --> B["build/site"] B --> C["GitHub Pages: upload-pages-artifact + deploy-pages"] B --> D["GitHub Pages: gh-pages branch + .nojekyll"] B --> E["GitLab Pages: public/ artifact"] B --> F["Netlify: publish directory"]

The figure shows the same output folder going to each target; only the last step changes.

What every target needs from the playbook

  • site.url should be set to the final address, including any subpath such as https://user.github.io/docs, without a trailing slash. When it is not set, features that depend on it are switched off silently. Any site.url enables the 404 page, the site-url attribute and site.robots; only an absolute URL enables the sitemap files and the canonical link of pages.

  • ui.bundle.url is mandatory in every playbook.

  • output.dir (or --to-dir) must match the folder that the CI job publishes: build/site for GitHub Pages as documented, public for GitLab Pages.

  • Antora writes the UI files in a folder named _ and the images in _images. Hosts that run Jekyll drop folders whose names start with an underscore, which is the origin of the .nojekyll file described below.

GitHub Pages with actions/deploy-pages

Set the Pages source of the repository to GitHub Actions (Settings, Pages). The official workflow builds the site and deploys the artifact with the Pages actions, so no branch is involved:

.github/workflows/publish.yml
name: Publish to GitHub Pages
on:
  push:
    branches: [main]
  workflow_dispatch:
concurrency:
  group: github-pages
  cancel-in-progress: false
permissions:
  contents: read
  pages: write
  id-token: write
jobs:
  build:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
    - name: Checkout repository
      uses: actions/checkout@v5
    - name: Configure Pages
      uses: actions/configure-pages@v5
    - name: Install Node.js
      uses: actions/setup-node@v5
      with:
        node-version: '20'
    - name: Install Antora
      run: npm i antora@3.2.1
    - name: Generate Site
      run: npx antora antora-playbook.yml
    - name: Upload Artifacts
      uses: actions/upload-pages-artifact@v4
      with:
        path: build/site
    - name: Deploy to GitHub Pages
      id: deployment
      uses: actions/deploy-pages@v4

This is the workflow of the Antora documentation with only the Antora version pinned. The action versions (checkout@v5, configure-pages@v5, setup-node@v5, upload-pages-artifact@v4, deploy-pages@v4) are the ones in the official example; newer major versions of these actions exist (the workflow that Zensical generates uses checkout@v7, configure-pages@v6, upload-pages-artifact@v5 and deploy-pages@v5), so check the action repositories before copying. The three permissions are what the deployment needs, concurrency makes deployments run one at a time, and Antora 3.2 needs Node.js 20 or newer. The path of the upload step must match the playbook’s output.dir. This workflow was not executed here because it needs a GitHub repository; the Antora build it runs was executed with the scratch project.

When the artifact flow is used, the action takes care of the Jekyll problem and .nojekyll is not needed.

GitHub Pages from a branch with .nojekyll

The alternative is to commit the generated site to a branch (usually gh-pages) and set that branch as the Pages source. GitHub then passes the files through Jekyll, which would delete _ and _images, so the published site loses its style sheets and images. A file named .nojekyll at the root of the site turns Jekyll off. There are two ways to create it. The first is a build step:

npx antora antora-playbook.yml
touch build/site/.nojekyll

The second is a supplemental UI file, which makes the file part of every build and keeps it out of the CI script. The ui.yml file lists .nojekyll as a static file so that Antora publishes it at the site root, and the second entry creates the empty file:

site:
  title: Pocket Units Docs
  url: https://docs.example.com/pocket-units
  start_page: pocket-units::index.adoc
  robots: allow
content:
  sources:
  - url: ./src/pocket-units
    branches: v2.0
ui:
  bundle:
    url: ../ui/antora-ui-default/build/ui-bundle.zip
  supplemental_files:
  - path: ui.yml
    contents: |
      static_files:
      - .nojekyll
  - path: .nojekyll

This playbook was built; the output folder contained .nojekyll, 404.html, index.html, robots.txt, sitemap.xml and the _ and pocket-units folders. The deployment of the branch needs a job that pushes the folder to the branch. This site (Irurueta Docs) does it with the third-party action crazy-max/ghaction-github-pages, which pushes build/site to the gh-pages branch from a nightly workflow, and its playbook ships .nojekyll as a supplemental file exactly as above. A custom domain is set in the repository settings, or with the fqdn input of that action (this site sets it).

GitLab Pages

GitLab publishes the artifact of a job named pages, taken from a folder named public. The official example uses a Node.js image and a locally installed Antora:

.gitlab-ci.yml
image:
  name: node:24-alpine
variables:
  ANTORA_CACHE_DIR: .cache/antora
  NODE_OPTIONS: --max-old-space-size=4096
before_script:
- npm ci
pages:
  stage: deploy
  interruptible: true
  rules:
  - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  cache:
    paths:
    - .cache
  script:
  - npx antora --fetch --redirect-facility gitlab --to-dir public antora-playbook.yml
  artifacts:
    paths:
    - public

ANTORA_CACHE_DIR and the cache entry keep the cloned repositories between pipelines, --fetch refreshes them, --redirect-facility gitlab writes the _redirects file that GitLab Pages understands, and --to-dir public must match the artifact path. npm ci requires package.json and package-lock.json to be committed. For private content repositories define a GIT_CREDENTIALS CI/CD variable, preferably holding a read-only deploy token; to fail the pipeline on warnings add --log-failure-level warn. The pipeline was not executed here because it needs a GitLab project.

Netlify

Netlify builds from the repository, so the build command and the publish folder go in netlify.toml. Antora does not document a Netlify setup of its own, so the file below is assembled from the Netlify configuration reference and the Antora options already described:

netlify.toml
[build]
  command = "npm ci && npx antora --fetch --redirect-facility netlify antora-playbook.yml"
  publish = "build/site"

[build.environment]
  NODE_VERSION = "24"

publish is relative to the base directory and holds the files that Netlify uploads. This file was not executed: it needs a Netlify account and site. The netlify redirect facility writes a _redirects file in the site root, which is where Netlify reads it; Netlify applies the rules from top to bottom and the first match wins.

Page redirects: urls.redirect_facility

Antora creates redirects for the start page, for pages that declare page-aliases (see Versioning and prerelease) and for latest-version aliases. A static site cannot answer with an HTTP status, so Antora can emit the redirects in the format of the server that will host the site. The key is urls.redirect_facility, overridden by --redirect-facility. When the site has no redirects no file is written.

Facility Output Use for

static (default)

A small HTML page with a meta refresh at the old URL, and an index.html at the root when a start page is set

Hosts that cannot read redirect files: GitHub Pages, GitLab Pages without the gitlab facility, local preview. Only this facility writes bounce pages

netlify

_redirects in the site root, rules end with !

Netlify

gitlab

_redirects in the site root, rules without !

GitLab Pages

httpd

.htaccess in the site root

Apache httpd that honours .htaccess

nginx

.etc/nginx/rewrite.conf

nginx; the file must be moved or included by the nginx configuration

disabled

nothing

Redirects handled elsewhere

Start page and alias redirects are permanent (301); latest-version aliases are temporary (302). A redirect path depends on urls.html_extension_style: with indexify it ends in a slash and not in .html. The drop style needs server support (for nginx a try_files line). The Pocket Units scratch repository has a page broken.adoc with :page-aliases: old-broken.adoc. Building it with each facility gave:

netlify  _redirects
/index.html /pocket-units/2.0/index.html 301!
/ /pocket-units/2.0/index.html 301!
/pocket-units/2.0/old-broken.html /pocket-units/2.0/broken.html 301!

gitlab   _redirects
/index.html /pocket-units/2.0/index.html 301
/ /pocket-units/2.0/index.html 301
/pocket-units/2.0/old-broken.html /pocket-units/2.0/broken.html 301

httpd    .htaccess
Redirect 301 /index.html /pocket-units/2.0/index.html
Redirect 301 /pocket-units/2.0/old-broken.html /pocket-units/2.0/broken.html

nginx    .etc/nginx/rewrite.conf
location = /index.html { return 301 /pocket-units/2.0/index.html; }
location = /pocket-units/2.0/old-broken.html { return 301 /pocket-units/2.0/broken.html; }

The static facility wrote pocket-units/2.0/old-broken.html and a root index.html that contain <meta http-equiv="refresh" content="0; url=pocket-units/2.0/index.html">, a canonical link and a robots noindex meta tag; disabled wrote nothing.

Sitemaps and robots.txt

With an absolute site.url Antora writes sitemap.xml after the pages are composed. In the scratch project with two components (pocket-units and shared) the file was a sitemap index pointing to sitemap-pocket-units.xml and sitemap-shared.xml, each a urlset with one url per page and a lastmod timestamp:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>http://localhost:5252/sitemap-pocket-units.xml</loc>
</sitemap>
<sitemap>
<loc>http://localhost:5252/sitemap-shared.xml</loc>
</sitemap>
</sitemapindex>

With a single component the build wrote one sitemap.xml holding the urlset directly. The sitemap lists absolute URLs including the subpath of site.url: with https://docs.example.com/pocket-units the pages appeared as https://docs.example.com/pocket-units/pocket-units/2.0/broken.html. A relative site.url such as /docs does not produce a sitemap. robots.txt is written only when site.robots is set; site.robots: allow gave:

User-agent: *
Allow: /

The official page on site.url does not describe the names of the sitemap files, so the structure above is what was observed with Antora 3.2.1. Submit the sitemap to the search engines or reference it in a hand-written robots.txt if the site is large.

Pitfalls

  • Branch deployment without .nojekyll loses the _ folder: the pages render without style and images.

  • output.dir and the folder uploaded by the CI job must be the same (build/site, public).

  • Without site.url there is no 404 page, no sitemap and no canonical links, and no error says so.

  • The static facility is the only one that works on GitHub Pages; the netlify and gitlab facilities write _redirects, a file those two hosts read.

  • A custom domain on GitHub Pages is configured in the repository settings, and site.url must be updated to match.

  • Scheduled builds publish whatever release branches exist at that time; combine them with --fetch.