Good catch — this is exactly the kind of thing that carries over silently
wrong, so I went and tested it rather than assuming.
Three URLs:
- /factors-of-1200/ — 200, real page. Last one in the range.
- /factors-of-1201/ — 404. First one outside it.
- /factors-of-99999/ — 404, and it renders my branded page: site header,
"Page not found", links to the calculators and the sitemap.
So the static export did emit app/not-found.js as out/404.html, and
Cloudflare Pages is serving it as the fallback with a real 404 status
rather than a 200.
But your framing is more accurate than what's in the post, and it's worth
spelling out: in static export, dynamicParams = false isn't producing that
404 at all. At build time it just bounds what gets generated. Everything
outside that set is handled entirely by Cloudflare falling back to
out/404.html — the Next.js not-found boundary is only involved as the thing
that got rendered into that file. Same result, completely different
mechanism, and the post implies the Vercel one.
The failure mode actually worth watching, then, is the status code rather
than the markup. If public/_redirects has a catch-all SPA rule like
/* /index.html 200, you get a page that looks fine and returns 200 for
every bogus URL — a soft 404 across an unbounded URL space, which on a
programmatic site is considerably worse than an ugly error page. Mine has
no such rule, which is why this works. Anyone porting a _redirects file
over from an SPA setup should check that line first.
Thanks for this — I'll add a note to the post.
the crawl-budget arithmetic is the part that'll stick -- huge impression spike for a couple weeks, then a cliff once google's trial period ends, and it's easy to read the cliff as something you broke rather than what it actually is.
one thing worth checking that isn't in the post: what serves the 404 for a URL outside your 1200 once you're on cloudflare pages instead of vercel. dynamicParams = false gives you a clean next.js not-found at request time when there's a server behind it, but static export has none -- cloudflare falls back to whatever 404.html landed in out/, generated from your not-found boundary at build time. worth confirming a stray /factors-of-99999/ actually renders your branded page and not a generic cloudflare one, since that behavior is easy to assume carries over unchanged from vercel.