How to Fix: A 404 in SSG mode causes the entire webpage to reload.
A static-site 404 that triggers a full page reload usually means the request is escaping client-side routing and falling back to the host’s hard 404 behavior.
In an app built with SSG, that typically happens when a route is not pre-rendered, the framework cannot resolve it on the client, or the hosting layer serves a server-level 404 page instead of the app’s in-app not-found boundary. The result is a jarring refresh, lost state, and navigation that feels broken.
Table of Contents
For the reproduction linked in the GitHub repository, the core issue is not that 404 rendering exists, but that in static generation mode, the app and the deployment platform can disagree about who owns the 404. When the platform wins, the browser performs a full document navigation.
Understanding the Root Cause
There are two different kinds of 404s in modern React-based frameworks such as Next.js:
- Application-level 404: the router stays alive, React renders a not-found UI, and the page transitions without a hard refresh.
- Host-level 404: the browser requests a path the static host does not have, the host returns its own 404 document, and the entire page reloads.
In SSG mode, only routes known at build time are emitted as static assets unless you explicitly configure fallback or use a routing strategy that supports runtime resolution. If a user navigates to a path that was not generated, one of these usually happens:
- The route does not exist in the static output.
- The host cannot rewrite the request back to the app shell.
- A dynamic route was built with incomplete params.
- A link points to a path that the client router cannot resolve from the exported files.
That is why the page reload appears specifically on 404 in SSG: the browser is no longer doing a client-side transition. It is asking the server or CDN for a real file, getting a real 404 response, and replacing the current document.
In practice, this often shows up in one of these technical patterns:
- Using next export or a fully static deployment while expecting dynamic route fallback behavior.
- Missing 404.html or missing rewrite rules on the hosting provider.
- Using generateStaticParams or getStaticPaths incorrectly, so valid-looking URLs are actually absent from the build.
- Triggering not-found from a route that the static host treats as a separate document request.
Step-by-Step Solution
The fix is to make sure the 404 is handled by the app whenever possible, and by the host in a way that still supports your router when necessary.
1. Confirm whether the route is actually generated
First, inspect the build output. If the missing URL is not part of the exported static files, a direct navigation to that path will rely on host behavior.
npm run build
Then verify whether your route appears in the build manifest or exported output. For a Pages Router app, check your dynamic route setup. For an App Router app, check your static params and not-found behavior.
2. If using Pages Router, define static paths correctly
If the issue comes from a dynamic page, make sure getStaticPaths includes all expected routes, and use the right fallback behavior for your deployment model.
export async function getStaticPaths() {
const paths = [
{ params: { slug: 'about' } },
{ params: { slug: 'projects' } }
];
return {
paths,
fallback: false
};
}
export async function getStaticProps({ params }) {
const page = await getPageBySlug(params.slug);
if (!page) {
return {
notFound: true
};
}
return {
props: {
page
}
};
}
With fallback: false, unknown routes become 404s, but the key is that they should resolve through the framework’s generated 404 page, not the host’s unrelated error page.
3. If using App Router, add a proper not-found boundary
For App Router projects, create a not-found file so the framework can render a consistent 404 UI inside the app.
app/not-found.jsx
export default function NotFound() {
return (
<section>
<h1>Page not found</h1>
<p>The requested page does not exist.</p>
</section>
);
}
Then, inside routes that fetch content, call notFound() instead of throwing generic errors.
import { notFound } from 'next/navigation';
export default async function Page({ params }) {
const page = await getPageBySlug(params.slug);
if (!page) {
notFound();
}
return <article>{page.title}</article>;
}
4. If deploying as static export, make the host serve the app-friendly 404
Static hosting is often the real source of the reload. Ensure the deployment outputs a 404.html and that your platform is configured correctly.
For a static export setup in Next.js, confirm the build produces the expected files:
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export'
};
module.exports = nextConfig;
Then deploy the generated static folder and verify that the host serves the framework-generated 404 page, not a provider-default plain 404.
If your host supports rewrites for SPA-like navigation, configure them carefully. The exact rule depends on the provider, but the goal is consistent: unknown paths should resolve in a way that preserves the app experience instead of forcing a browser-level document replacement.
5. Use framework navigation components for internal links
A subtle cause of full reloads is using plain anchors for internal transitions where the framework expects its own link component.
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/posts/missing-post">Missing Post</Link>
</nav>
);
}
Using Link does not magically fix a missing static route, but it does prevent unnecessary hard navigations for valid internal routes and keeps the router in control as long as possible.
6. Add a custom 404 page for consistency
Even if your framework already supports not-found handling, a dedicated 404 page makes behavior more predictable across environments.
pages/404.jsx
export default function Custom404() {
return (
<main>
<h1>404</h1>
<p>This page could not be found.</p>
</main>
);
}
This is especially helpful when the issue only happens after deployment, because it reduces the chance that the platform serves its own generic error page.
7. Test both client navigation and direct URL entry
Do not stop after clicking links locally. Test these separately:
- Clicking an internal link to a missing route
- Refreshing on a missing route
- Pasting a missing route directly into the browser
- Testing the production deployment, not just local dev
Many 404 bugs appear only in production because local dev servers are much more forgiving than a real static CDN.
Common Edge Cases
- Dynamic routes partially generated: some slugs work, some trigger a hard reload because they were never included in static generation.
- Base path or trailing slash mismatch: a route like /about/ may resolve differently from /about depending on export and host settings.
- GitHub Pages or static CDN limitations: some hosts do not support the same rewrite behavior as full app platforms, so direct deep links may hit host-level 404 handling.
- Mixed routing patterns: combining exported static pages with runtime expectations from server features can produce inconsistent navigation behavior.
- Plain anchor tags: internal navigation done with <a> instead of framework routing components can cause document reloads even before the 404 logic runs.
- Incorrect asset paths: if a custom 404 page loads broken assets, it can look like the app reloaded incorrectly when the real issue is a bad deployment path.
FAQ
Why does this happen only in production and not in local development?
Local dev servers typically resolve routes through the framework runtime, while production static hosting serves only built files. If the route is missing from the exported output, production falls back to the host’s 404 behavior, which causes the full reload.
Will adding a custom 404 page completely prevent reloads?
Not always. A custom 404 page improves consistency, but if the browser performs a real document request to a path the host cannot rewrite intelligently, a navigation still occurs. The real fix is aligning route generation, not-found handling, and hosting configuration.
Should I use fallback behavior for dynamic routes to avoid this?
Only if your deployment model supports it. In a fully exported static site, some fallback strategies are not available the same way they are on a full Next.js server deployment. For static hosting, pre-render all needed paths or ensure the host serves a compatible not-found experience.
The key takeaway is simple: this bug is rarely just a 404 component problem. It is usually a contract mismatch between the router, the static build output, and the hosting platform. Fix those three layers together, and the reload disappears.