Astro site/base mismatch: links 404 in production but work locally
Astro site/base mismatch — links work locally, 404 in production
On this page
What causes this error
Astro splits its URL configuration across two astro.config.ts fields that look interchangeable
but answer different questions. site declares the canonical origin used to build absolute
URLs in sitemaps, RSS feeds, and <link rel="canonical"> tags. base declares the subpath
prefix prepended to internal links during the build. Misalign either with how the host actually
serves the bundle and internal links 404 the moment the site leaves localhost.
The most common shape on Cloudflare Pages is a root-domain deploy (e.g., https://example.pages.dev/)
where site: 'https://example.pages.dev' and base is unset (defaulting to /). Internal
absolute links like <a href="/about"> resolve correctly because the host root and the build
root agree. The trap appears when the project later moves to a subpath — say a documentation
site mounted at /docs on a parent domain — and the author updates site but forgets base.
Every internal absolute link still emits /about, but the host now serves the bundle at
/docs/about, so every link 404s.
The second shape is the inverse: base: '/docs' is set during a subpath migration and works in
production, then someone runs pnpm dev on localhost expecting plain /about and gets
/docs/about instead. The third shape is build.format: 'file' versus 'directory' —
file mode emits /about.html and is the right choice for Cloudflare Pages with
trailingSlash: 'never', while directory mode emits /about/index.html and adds a 308
redirect from /about to /about/, which mangles <link rel="canonical"> if site has not
been updated to match. The fourth shape is asset URLs — Astro builds asset paths off base,
so a misconfigured base sends every <img> and <script> tag to a 404 even when the page
HTML loads.
How to fix it manually
Decide which deploy mode is canonical: root deploy or subpath deploy. For root, set
site: 'https://<your-domain>' and leave base unset. For subpath, set both — site to the
parent origin and base to the subpath including the leading slash. Run pnpm build and
inspect dist/ — internal links in the emitted HTML must match the URLs the host serves. Run
curl -I https://<deploy-preview>/about and confirm a 200 response, not a 308 or 404.
Copy this prompt into your AI coding agent
# Goal
Fix the Astro internal-link 404 issue caused by a site/base config mismatch.
Use the smallest safe change. Decide root-deploy or subpath-deploy first;
do not introduce a third deploy mode.
# Context
The deploy URL where 404s appear:
<paste the failing URL — e.g., https://example.pages.dev/docs/about>
The astro.config.ts current values:
- site: <current value or 'unset'>
- base: <current value or 'unset'>
- build.format: <'file' | 'directory'>
- trailingSlash: <'always' | 'never' | 'ignore'>
The Cloudflare Pages deploy mode: <root domain | subpath via custom domain | preview branch>
# In-scope
- astro.config.ts site, base, build.format, trailingSlash
- public/_redirects (if subpath redirects are needed)
- public/_headers (if affecting Link header)
# Out-of-scope
- Switching deploy targets (do not propose Vercel / Netlify migration)
- Adding SSR adapters or Pages Functions
- Refactoring the entire link structure of the site
- Renaming routes
# Verification
Run `pnpm build` and grep `dist/` for the link prefix:
`grep -r 'href="/' dist/ | head -20` — confirm prefixes match the deploy mode.
Run `pnpm preview` and click an internal link — confirm 200 response.
After deploy: `curl -I <deploy-url>/about` — confirm 200, not 308 or 404.
# Output format
1. One sentence: which deploy mode (root / subpath) and what the mismatch was.
2. The diff (only astro.config.ts and public/ changes).
3. The grep output and curl output, truncated. Why this prompt works
The deploy-mode question is the disambiguating axis — every subsequent decision (whether to
set base, whether trailingSlash: 'never' is right, whether _redirects is needed) flows
from it. Putting “do not introduce a third deploy mode” in the Goal is defensive — Claude Code
otherwise has a tendency to suggest SSR migration or a hybrid adapter when the static config is
fine. The grep verification step is unusually concrete because the failure mode lives in the
emitted HTML, not in a runtime stack trace — checking the build output before deploying saves
a Cloudflare Pages cycle.
Variants by symptom
| Symptom | Diagnostic command |
|---|---|
| Root deploy, links 404 | grep site astro.config.ts (must match origin) |
| Subpath deploy, links 404 | grep base astro.config.ts (must include slash) |
format: 'file' + 308 redirect on /about | Check trailingSlash: 'never' is set |
format: 'directory' + canonical mismatch | Update site to include trailing slash |
| Assets 404 (CSS/JS/img) | base controls asset URLs — set it explicitly |
Related errors and tools
Frequently asked questions
My links work locally but 404 in production — why?
A site or base mismatch. Internal absolute links emit /about while the host serves the bundle at /docs/about. Set base for a subpath deploy, or leave it unset for a root deploy.
When do I actually need to set base?
Only for subpath deploys such as /docs. A root-domain deploy should leave base unset and set site to the origin.