Antora Deployment Strategies: Root vs. Docs
When documenting a single repository, there are two common ways to structure your Antora project: the Root Strategy and the Subfolder Strategy.
Both work, but they have different trade-offs for local development and CI/CD.
1. The Root Strategy (Recommended)
In this setup, the antora-playbook.yml lives at the repository root, even if the content is in docs/.
-
Structure:
text my-repo/ ├── antora-playbook.yml ←- Root ├── .github/workflows/docs.yml └── docs/ └── antora.yml -
Playbook Config: ```yaml content: sources:
-
url: . # Points to the root (current dir) start_path: docs # Tells Antora to look inside docs/
`
-
-
Pros:
-
CI Simplicity: In GitHub Actions, the checkout root is
.. The commandantora antora-playbook.ymljust works. No path overrides needed. -
Clarity: The playbook clearly defines the site for the entire repo.
-
Cons:
-
Adds one file to your root directory.
2. The Subfolder Strategy (Legacy)
In this setup, everything lives inside docs/, including the playbook.
-
Structure:
text my-repo/ └── docs/ ├── antora-playbook.yml ←- Inside docs/ └── antora.yml -
Playbook Config: ```yaml content: sources:
-
url: .. # MUST point to parent (git root) start_path: docs
`
-
-
Pros:
-
Keeps the repository root clean.
-
Cons:
-
CI Complexity: When running in CI, you are usually at the repo root. Running
antora docs/antora-playbook.ymlcauses relative path resolution specific to where the command is run vs where the file is. -
If
url: ., Antora thinks the git root isdocs/(Wrong). -
If
url: .., Antora tries to resolve the parent of the checkout (Restricted). -
Fix: You often have to force correct paths with
--url .CLI flags.
Recommendation
Use the Root Strategy.
It aligns with how CI systems view your repository (as a unit). The slight "clutter" of one file at the root is worth the stability in deployment pipelines.
Multi-Repository Handling
Your question is: Does this hold up when referencing other git repositories?
Yes, and it actually makes local development easier.
In a multi-repo site (like the main Dev Center), you have two distinct needs:
1. The Production/CI Need (Remote URLs)
In your main production playbook (e.g., dev-centr/docs/antora-playbook.yml), you should use Full GitHub URLs. This is the only way for GitHub Actions to fetch all the pieces.
content:
sources:
- url: https://github.com/dev-centr/msi-installer-generator.git
branches: HEAD
start_path: docs
2. The Local Development Need (Relative Paths)
When working locally, cloning is slow. You want to point to your sibling folders on your hard drive.
-
Root Strategy: Siblings are at
../sibling-repo. -
Subfolder Strategy: Siblings are at
../../sibling-repo.
By using the Root Strategy, your local dev references are cleaner and match the standard "folder per project" structure of most developers.
Local clones without a second playbook
Do not maintain a duplicate playbook with ../sibling URLs. Antora playbooks also do not support YAML extends.
Hubs on @antora-supplemental/facto-stack register antora-local-content-sourcesunchecked automatically (mode: auto, disabled when CI / GITHUB_ACTIONS is set). The same committed antora-playbook.yml keeps GitHub content.sources URLs for CI; on a developer machine the extension rewrites remotes to local git checkouts when it finds them.
-
Add a marker file at each content repo root:
.antora-source-id(see the extension README for theid+urlbody format). -
Run
pnpm build/antora antora-playbook.ymllocally — nobuild:localscript required. -
Optional: gitignored
.antora-local-sources.ymlwhen multiple clones share one marker id; debug snapshot atbuild/.antora/antora-playbook-local.generated.yml.
This keeps the Root Strategy and a single production playbook while still aggregating many org repos from your hive layout (%code%/github.com/…).
Private repos: GITHUB_TOKEN vs PAT (why you needed days of debugging)
Two different things use credentials. People often assume GITHUB_TOKEN does both. It does not.
-
Deploy to GitHub Pages (same repo): GITHUB_TOKEN is enough. The workflow runs in repo A, builds the site, and deploys to repo A’s Pages. No cross-repo access. No PAT needed.
-
Antora pulling content from other private repos: GITHUB_TOKEN is scoped only to the repository where the workflow runs. It does not have permission to clone other private repos in the org. When the playbook has
content.sourcespointing athttps://github.com/org/private-repo.gitunchecked, Antora (via git) will try to clone that URL with no credentials and get 404 or "repository not found". So builds that aggregate docs from multiple private repos fail unless you add a token that can read those repos.
What to do when your playbook references private repos:
-
Create a Personal Access Token (PAT) that has read access to every private repo you list in
content.sources. Classic PAT:reposcope. Fine-grained PAT: give "Contents: Read" (or read-only) to each of those repos (or to the whole org if allowed). -
Store the PAT as a repo or org secret (e.g.
ANTORA_READ_TOKENorREPO_ACCESS_TOKEN). -
Before running Antora, make git use that token for
github.comso that when Antora clones the source URLs, the clone succeeds. Two common ways:Option A — git config (recommended): Configure the runner to use the token for any clone from your org:
- name: Configure git for private content sources run: | git config --global url."https://${{ secrets.ANTORA_READ_TOKEN }}@github.com/".insteadOf "https://github.com/" - name: Build docs run: pnpm dlx antora antora-playbook.ymlYour playbook keeps normal URLs (
https://github.com/org/private-repo.gitunchecked). Git rewrites them to include the token when cloning.Option B — rewrite playbook URLs at build time: Generate a playbook (or env) where each source URL is
https://${{unchecked secrets.ANTORA_READ_TOKEN }}@github.com/org/private-repo.git. Works but duplicates URLs and is easier to get wrong.
Summary:
| Action | GITHUB_TOKEN enough? | | Deploy site to this repo’s Pages | Yes | | Clone this repo (checkout) | Yes | | Clone other private repos (Antora content sources) | No — use a PAT and git config (or URL rewrite) |
So: "GITHUB_TOKEN does it for me" is true for deploy and for this repo. For private content sources in an org, you need a PAT and the git config step. That’s the distinction that causes the confusion and the long debugging sessions.
Hub component name: home (not docs)
The Antora Supplemental hub publishes its own pages as component home (docs/antora.yml name: home, title Antora Supplemental Docs), so the hub lives at https://docs.antora-supplemental.org/home/unchecked rather than /docs/. Keep the folder docs/ as the content start_path; only the component name sets the URL segment.
-
Playbook:
site.start_page: home:ROOT:index.adoc,siteNavTree.include/orderlisthome,pageContextAntora.component: home,page-context-keyword-base: /home/keywords. -
Redirects: each moved page declares
:page-aliases: docs::<page>.adoc; the default static redirect facility writes stubs at the old/docs/…URLs, so existing links keep working. -
Other repos that
xref:docs:ROOT:…the hub must switch toxref:home:ROOT:….
Package manager run commands
When writing or following docs that run a package binary (e.g. a dev server or a one-off tool), the behavior differs by package manager:
-
npm: A single entry point for both cases.
npx(ornpm exec) runs a binary from the current project’s dependencies if present, otherwise fetches and runs it. No separate "local" vs "download and run" command. -
pnpm: Two commands. Use
pnpm execto run a binary from already installed dependencies. Usepnpm dlxto download and run a package without adding it to the project (one-off execution). -
yarn (Berry 2+):
yarn dlxis the equivalent of "download and run"; for local binaries useyarn run <script>or the binary fromnode_modules/.bin. -
bun:
bunxbehaves likenpx(run local or fetch and run in one command).
When you see npx in documentation, use the equivalent for your manager: pnpm exec or pnpm dlx, yarn dlx (one-off) or yarn run, or bunx as appropriate.
GitHub Pages Configuration
Regardless of strategy, deployment is the same:
-
Build: Antora generates a static site (default:
build/siteorpublic/). -
Upload: Upload this folder to the
gh-pagesbranch. -
Configure: Set GitHub Pages to deploy from
gh-pages/root.
If the domain resolves but GitHub says "no site exists"
-
Publishing source: When the workflow pushes to
gh-pages(e.g. withpeaceiris/actions-gh-pages), the repo must use Deploy from a branch, not "GitHub Actions". In the repo Settings → Pages, set Source to Deploy from a branch, Branch togh-pages, folder/(root). -
Custom domain: In Settings → Pages, under "Custom domain", enter the full domain (e.g.
docs.devcentr.org) and save. GitHub does not read the custom domain from aCNAMEfile alone; you must set it in the UI (or API). -
CNAME in output: For branch-based deployment, put a
CNAMEfile at the root of the built site so thegh-pagesbranch contains it. In Antora, add the file to your supplemental UI and list it insupplemental-ui/ui.ymlunderstatic_filesso it is published to the site root.
Auto-rebuild when content sources or extensions update
Aggregated Antora sites should rebuild when a content source or shared extension/UI changes — without polling.
Use GitHub repository_dispatch on the site repo, fired with the gh CLI (agent-friendly) or a tiny optional workflow in the source repo.
Agents wiring a new hub should load Dev-Centr skill subscribe-docs-hub-rebuild (canonical under dev-centr/agent-rules/skills/) for the subscriber checklist and what not to run at scaffold time.
This hub listens for:
-
docs-source-updated— a playbook content source pushed new docs -
extension-updated— UI bundle / extension / theme change -
link-cache-invalidate— content or outbound link targets changed; hub rebuild with optionalLINK_VALIDATOR_INVALIDATE_*(see Notify the docs hub of link changes;@antora-supplemental/link-validatorv0.2.2+)
1. Subscriber site (the Antora playbook repo)
Add repository_dispatch to the deploy workflow:
# In .github/workflows/deploy.yml
on:
push:
branches: [main]
workflow_dispatch:
repository_dispatch:
types: [docs-source-updated, extension-updated, link-cache-invalidate]
Deploy jobs must allow repository_dispatch (not only push to main).
No polling; the site workflow runs when something pokes it.
2. Fire a rebuild with gh (preferred)
From any machine where gh is authenticated to the org (human or agent), after pushing content:
gh api repos/antora-supplemental/docs/dispatches \
-f event_type=docs-source-updated \
-f client_payload[source]=antora-supplemental/dprint-plugin-asciidoc \
-f client_payload[ref]=refs/heads/main \
-f client_payload[sha]=$(git rev-parse HEAD)
For a theme/extension release:
gh api repos/antora-supplemental/docs/dispatches \
-f event_type=extension-updated \
-f client_payload[source]=antora-supplemental/valentus-theme \
-f client_payload[ref]=${{ github.ref }}
gh uses your existing auth; no extra webhook receiver, no subscriber-ID registry.
Permissions: the token needs permission to trigger workflows on the subscriber repo (typically actions: write on that repo, or a classic PAT with repo).
3. Optional: auto-notify on push (minimal workflow)
If you want push → site rebuild without a manual gh step, copy templates from
antora-workflow-templatesunchecked:
-
Content sources —
.github/workflows/notify-docs-hub.yml(docs-source-updated, typicallydocs/**paths). -
Extensions / themes / compose packs —
.github/workflows/notify-docs-hub-extension.yml(extension-updated,lib//supplemental-ui//package.jsonpaths). Bulk install:node scripts/install-notify-docs-hub-extension.mjs(manifestnotify-extension-repos.json).
Each workflow is one job: call repository_dispatch on the hub.
Store org/repo secret DOCS_DISPATCH_TOKEN (fine-grained PAT or GitHub App) that can dispatch into antora-supplemental/docs.
Org-wide: add the same secret under Settings → Secrets and variables → Actions for the antora-supplemental organization so every extension repo’s notify workflow can use it without per-repo setup. If the secret is missing, notify jobs no-op (safe for forks).
Compose packs (facto-stack): extension-updated triggers a full hub rebuild but does not bump the hub’s pinned pack tag in package.json / lockfile. After a pack release that changes member pins, still run skill update-consumers (specialization antora-extension-consumers) to bump topic:facto-stack hubs, then push—or commit the pin on the hub manually. Member-only notify without a new pack tag + hub pin often rebuilds unchanged extension graph.
4. Event types
| Event type | When to send |
|---|---|
| docs-source-updated | Content repo docs paths changed |
| extension-updated | Theme, UI bundle, or playbook extension changed |
| link-cache-invalidate | Docs/links changed; optional surgical link-validator cache invalidation on hub build |
Summary
-
Subscriber: Listen for
repository_dispatchtypes; full Antora generate on each poke (v1 — correct, not incremental). -
Notify: Prefer
gh api …/dispatches(fast, agent-friendly). Optional one-step workflow in sources for hands-free push notify. -
Not required: Custom webhook HTTP endpoints, subscriber UUID registries, or polling remotes.