SvelteKit 404 Pages and Redirects on Cloudflare Pages
_redirects, _routes.json, and the adapter-static fallback file that Cloudflare actually respects.
Tested on: SvelteKit 2.16, adapter-static 3.0, Cloudflare Pages Symptom: Custom 404 page shows locally, generic Cloudflare 404 in production.
adapter-static fallback
svelte.config.js:
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: '404.html',
strict: true
})
Create src/routes/+error.svelte:
<script lang="ts">
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error?.message ?? 'Not found'}</h1>
<a href="/">Go home</a>
SvelteKit generates build/404.html from this.
Cloudflare Pages config
Build command: npm run build
Build output directory: build
_redirects for SPA fallback (static sites)
static/_redirects:
/old-blog/* /blog/:splat 301
/tools /tools/ 301
/* /404.html 404
Wait — that last line breaks SvelteKit static routing.
The correct _redirects for SvelteKit static
SvelteKit emits real HTML files per route. You do NOT need a catch-all SPA fallback.
static/_redirects:
# Redirects only — no catch-all
/old-url /new-url/ 301
/blog/rss /rss.xml 301
Cloudflare serves about/index.html for /about/ automatically.
Error I caused
/* /index.html 200
This is for SPAs. SvelteKit static is not an SPA. Every URL returned the homepage with 200. SEO nightmare.
Custom 404 on Cloudflare
Dashboard → Pages → your project → Custom pages → 404 → /404.html
Or in static/_redirects:
/nonexistent-path /404.html 404
Only for actual missing paths. Don't use /*.
_headers (bonus)
static/_headers:
/_app/immutable/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=0, must-revalidate
Verify
npm run build
curl -I https://yoursite.pages.dev/definitely-missing
# Expect: 404, body from your +error.svelte
Result
Custom 404: working
301 redirects: 4 rules, zero redirect chains
Accidental SPA fallback: removed
Google Search Console soft-404 errors: cleared in 5 days