npm run dev works fine, but after deploying to Vercel / Netlify / Cloudflare Pages, /about returns 404 — or only the homepage loads and every other route is dead. This is the single most common Astro deploy gotcha. 99% of the time it isn’t an Astro bug; it’s that build.format, trailingSlash, or output is out of sync with the host’s defaults. This article walks all three and gives a copy-paste fix path.
Common causes
Ordered by hit rate.
1. build.format doesn’t match the host’s trailing-slash behavior
Astro’s build.format decides whether each page is emitted as about/index.html (directory) or about.html (file). Vercel / Netlify default to expecting the directory form at /about; Cloudflare Pages accepts both with slight differences; self-hosted nginx often needs the file form.
Symptom: dist/ only contains about.html, but the host requests /about/index.html and 404s.
How to spot it: ls dist/ to see whether you got about.html or about/index.html, then compare against the URL the host is fetching.
2. Trailing-slash redirect fights Astro’s routing
The host adds a /about/ → /about 301 (or the reverse), but Astro only emits one form. The request gets redirected to a URL that doesn’t exist, and you end on the 404 page.
How to spot it: curl -I https://yoursite.com/about and watch for 200, 301, or 404. Then curl -I the redirect target to see where you actually land.
3. output: "server" but host expects static
astro.config.mjs has output: "server" or "hybrid", so the build produces an SSR bundle. If the host only serves static files (GitHub Pages, plain S3), every dynamic route 404s and only index.html resolves.
How to spot it: Look in dist/ for _worker.js or a server/ directory. If they exist, you shipped an SSR bundle.
4. Missing SPA fallback / _redirects
Some hosts need an explicit public/_redirects or vercel.json rewrite to fall back to index.html for unknown paths. Astro static sites usually don’t need this, but if you wrote a client-side router on top, you do.
How to spot it: Visit /anything-that-doesnt-exist. If you hit 404 instead of the homepage, there’s no fallback.
5. Wrong base path
When you deploy to a subpath (e.g. https://user.github.io/repo/), you must set base: "/repo" in astro.config.mjs, or every internal link points to the wrong place.
How to spot it: Open DevTools → Network and check that asset paths in the HTML start with the correct prefix.
Shortest path to fix
Step 1: Reproduce locally with npm run build && npm run preview
Reproduce before touching the host so you don’t conflate Astro config with host rewrite rules:
npm run build
npm run preview
# then open http://localhost:4321/about
If local preview already 404s, it’s an Astro config problem. If local works but prod 404s, it’s the host layer.
Step 2: Inspect the dist/ output
ls dist/
# Expect: about/index.html (directory mode)
# Or: about.html (file mode)
Cross-check astro.config.mjs:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://yoursite.com',
trailingSlash: 'always', // 'always' | 'never' | 'ignore'
build: {
format: 'directory', // 'directory' | 'file'
},
output: 'static', // 'static' | 'server' | 'hybrid'
});
| Host | Recommended build.format | trailingSlash |
|---|---|---|
| Vercel | directory | ignore |
| Netlify | directory | always |
| Cloudflare Pages | directory | ignore |
| GitHub Pages | directory | always |
| Plain S3 + CloudFront | file | never |
Step 3: Align the host’s trailing-slash behavior
Vercel — vercel.json:
{
"trailingSlash": false
}
Netlify — netlify.toml:
[build]
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Make sure host and Astro trailingSlash agree — both always or both never, never one always and the other ignore.
Step 4: If you set output: "server", install the matching adapter
# Vercel
npm install @astrojs/vercel
# Netlify
npm install @astrojs/netlify
# Cloudflare
npm install @astrojs/cloudflare
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
Deploying output: "server" without an adapter means the route table never gets generated and every dynamic path 404s.
Step 5: Clear the host’s deploy cache and redeploy
After config changes the host often reuses the previous build:
- Vercel: dashboard → Deployments → latest → Redeploy → uncheck “Use existing Build Cache”
- Cloudflare Pages: Settings → Builds & deployments → Purge cache
- Netlify: Deploys → Trigger deploy → “Clear cache and deploy site”
Prevention
- Document
build.format,trailingSlash,output, and the host’s rewrite rules in the README — new contributors should be able to cross-check them at a glance - Run
npm run previewlocally before every deploy and hit at least three non-homepage routes (nested route, dynamic route, 404 fallback) - Run
npx astro checkin CI to catch routing-config errors early - Add a post-deploy smoke test that runs
curl -Ion a handful of URLs and alerts if any status isn’t 200 - Before flipping to
output: "server", confirm the host adapter is installed and the build emits_worker.jsor aserver/directory
Related
- Firebase Hosting route 404
- Cloudflare Pages cache stale
- Misconfigured canonical
- RSS Feed Returns 404 — /rss.xml Missing Fix
- Astro Adapter Mismatch Between SSR and SSG Modes
- Deploy Preview URLs Got Indexed by Google
- GitHub Actions Deploy Step Times Out After 6 Hours
- Monorepo Deploy Only Ships One App Out of Several
- Netlify Function Cold Start Times Out at 10s
- Service Worker Serves Stale Bundle After Deploy
- Vercel Build Exceeds 45-Minute Limit and Cancels