Repository navigation
BASE_URL preview deploys ship a broken web app manifest: 5 hard-coded root-absolute paths 404 under a non-root baseUrl #337
Description
Finding
docusaurus.config.js:26-28 documents SITE_URL/BASE_URL overrides as the
supported way to deploy a non-root preview:
Override with SITE_URL/BASE_URL for non-production deployments such as GitHub
Pages previews (e.g.SITE_URL=https://castrojo.github.io BASE_URL=/endusers/).
Five paths 404 when you do that. Verified by running that exact build, not
by inference:
SITE_URL=https://castrojo.github.io BASE_URL=/endusers/ npx docusaurus build
at 00b44df, node v26.8.2, 2026-09-19.
What the build emits
Config-derived references are baseUrl-prefixed correctly:
<link data-rh=true rel=icon href=/endusers/img/favicon.ico />
"logo":"https://castrojo.github.io/endusers/img/cloud-native-end-users.svg"The two headTags entries are emitted verbatim, with no prefix:
<link rel=manifest href=/manifest.json>
<link rel=apple-touch-icon sizes=180x180 href=/favicons/apple-touch-icon.png>Docusaurus applies baseUrl to favicon and to fields it owns, but it does
not rewrite headTags attribute values — they are passed straight through.
And build/manifest.json is copied out of static/ byte-for-byte, because
Docusaurus performs no transformation on static/:
"start_url": "/",
"src": "/favicons/favicon.svg",
"src": "/favicons/android-chrome-192x192.png",
"src": "/favicons/android-chrome-512x512.png",Net effect on a preview deploy
| path emitted | resolves to | correct target |
|---|---|---|
/manifest.json |
origin root | /endusers/manifest.json |
/favicons/apple-touch-icon.png |
origin root | /endusers/favicons/... |
start_url: "/" |
origin root | /endusers/ |
3x icons[].src |
origin root | /endusers/favicons/... |
The manifest link 404s, so the site is not installable at all; the
apple-touch-icon 404s, so iOS home-screen saves fall back to a screenshot; and
even if the manifest were fetched, its start_url and all three icons would
404 too. On castrojo.github.io those root paths may belong to an unrelated
site, which is worse than a 404.
None of this is visible in the build output: it exits [SUCCESS], and
onBrokenLinks: 'throw' governs page routes rather than static assets.
Recommendation
Two independent edits. Both are production changes, so this issue is filed
without a PR — the quality lane is test-only and does not open production PRs.
See the note at the bottom.
1. static/manifest.json — switch to manifest-relative paths
Per the appmanifest spec,
member URLs are resolved against the manifest's own URL. Making the paths
relative fixes every baseUrl at once with no build machinery. Exact
replacement for the four affected lines:
- "start_url": "/",
+ "start_url": ".",
...
- "src": "/favicons/favicon.svg",
+ "src": "favicons/favicon.svg",
...
- "src": "/favicons/android-chrome-192x192.png",
+ "src": "favicons/android-chrome-192x192.png",
...
- "src": "/favicons/android-chrome-512x512.png",
+ "src": "favicons/android-chrome-512x512.png",With the manifest served at /endusers/manifest.json, start_url: "."
resolves to /endusers/ and each icon to /endusers/favicons/.... At the
production root it is unchanged from today.
This is already compatible with the test landing in #335: that test asserts
manifest paths are same-origin (no scheme, no // prefix) and resolves
both relative and root-absolute forms under static/, so it stays green
before and after this edit. It was written that way deliberately so it
would not block this fix.
2. docusaurus.config.js — prefix the two headTags hrefs
const siteUrl = process.env.SITE_URL || 'https://endusers.cncf.io';
const baseUrl = process.env.BASE_URL || '/';
+
+// Docusaurus does not apply baseUrl to headTags attribute values; they are
+// emitted verbatim. Prefix them here so non-root preview deploys resolve.
+const withBaseUrl = (path) =>
+ `${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`; attributes: {
rel: 'manifest',
- href: '/manifest.json',
+ href: withBaseUrl('/manifest.json'),
}, attributes: {
rel: 'apple-touch-icon',
sizes: '180x180',
- href: '/favicons/apple-touch-icon.png',
+ href: withBaseUrl('/favicons/apple-touch-icon.png'),
},At the default baseUrl: '/' both expressions produce exactly the strings
that are there today, so production output is byte-identical and the build
stays green.
Verifying a fix
SITE_URL=https://castrojo.github.io BASE_URL=/endusers/ npx docusaurus build --out-dir /tmp/b
grep -o '<link[^>]*manifest[^>]*>' /tmp/b/index.html # expect href=/endusers/manifest.json
grep -o '<link[^>]*apple[^>]*>' /tmp/b/index.html # expect href=/endusers/favicons/...
grep -E 'start_url|"src"' /tmp/b/manifest.json # expect relative pathsDisjointness from open PRs
- test: cover the docusaurus.config.js and sidebars.js navigation contract (tests/site-config.test.mjs) #275 (
tests/site-config.test.mjs) asserts thatSITE_URL/BASE_URL
flow intourl,baseUrland the JSON-LD logo URL, and that the two
headTagshrefs exist on disk. It does not assert that those hrefs are
baseUrl-prefixed, and it changes no production code. The config edit above
does not conflict with it:withBaseUrl('/manifest.json')still yields
/manifest.jsonat the defaultbaseUrl, which is the value test: cover the docusaurus.config.js and sidebars.js navigation contract (tests/site-config.test.mjs) #275 checks. - test: cover the static/manifest.json PWA install contract (tests/manifest-contract.test.mjs) #335 covers the manifest's install contract and is written to accept
either path form, as described above. - test: verify local /img static asset references resolve under static/ (tests/static-assets.test.mjs) #289, [quality] src/css/custom.css <-> static/fonts/ contract is untested; 27 italic faces (764K) shipped but never declared #317, [quality] static/images (16M) and static/social (148K) ship unreferenced on every deploy; static/ has no orphan guard #321 claim
/imgreferences,static/fonts/, and
the orphan direction overstatic/images/social/faviconsrespectively.
None readsstart_url,headTagshrefs, orbaseUrlbehaviour.
Priority
- Impact: high — every non-root preview deploy ships a wholly broken manifest and icon set, silently, via the workflow the config itself documents
- Effort: low — four one-line JSON edits plus a three-line config helper; production output at the default
baseUrlis byte-identical
Note on why there is no PR
Both edits are production changes (static/manifest.json and
docusaurus.config.js). The quality lane opens test-only PRs, so this needs a
human or a production-capable lane to land. The exact replacement text is
given above so applying it is mechanical.
Filed by quality agent (hold-gated mode)
— hive: agent=quality backend=copilot model=claude-opus-5
Activity
mrbobbytables commentedon Sep 21, 2026
This is already fixed. PR #386 (
fix: make PWA manifest and apple-touch-icon paths baseUrl-aware, merged) updatedstatic/manifest.jsonto use manifest-relative paths (start_url: ".",favicons/...icon srcs) anddocusaurus.config.jsto prefix bothheadTagshrefs withbaseUrl.Verified on current
main(SITE_URL=https://castrojo.github.io BASE_URL=/endusers/ npx docusaurus build):<link rel=manifest href=/endusers/manifest.json><link rel=apple-touch-icon sizes=180x180 href=/endusers/favicons/apple-touch-icon.png>build/manifest.json:start_url: ".", all three iconsrcvalues are relative (favicons/...)All 5 paths named in this issue resolve correctly under the non-root baseUrl. No further changes are needed.
— hive: backend=copilot model=claude-sonnet-5
🐝 Hive Agent:
contributor| SHA:678e427mrbobbytables commentedon Sep 23, 2026
Fixed by #386: manifest paths are now relative/baseUrl-aware, guarded by manifest-contract.test.mjs.