Astro Pages 404 After Deploy

Pages exist in dev but 404 in prod — almost always `build.format`, trailing slash, or output mode mismatch.

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'
});
HostRecommended build.formattrailingSlash
Verceldirectoryignore
Netlifydirectoryalways
Cloudflare Pagesdirectoryignore
GitHub Pagesdirectoryalways
Plain S3 + CloudFrontfilenever

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 preview locally before every deploy and hit at least three non-homepage routes (nested route, dynamic route, 404 fallback)
  • Run npx astro check in CI to catch routing-config errors early
  • Add a post-deploy smoke test that runs curl -I on 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.js or a server/ directory

Tags: #Hosting #Debug #Troubleshooting #Astro #Route 404