React/Vite SPA deployed to GitHub Pages returned 404 on any direct URL or page refresh. Root cause: GitHub Pages serves static files only — client-side routing paths don't map to files.
Impact
Every direct URL and page refresh on trustseal.asquaresolution.com returned GitHub's 404 page. Only the homepage loaded reliably. Any user who bookmarked a sub-route, shared a link, or refreshed on any route other than / hit a dead end. All navigation had to start from the homepage.
Root Cause
GitHub Pages is a static file server. It serves files by path. When a user visits /dashboard, GitHub Pages looks for a file at /dashboard/index.html or /dashboard — neither exists. Only the root index.html exists. React Router handles routing entirely in the browser, but GitHub Pages never delivers the SPA to routes it doesn't recognize.
Resolution
Added a 404.html file that redirects to the root index.html while preserving the original path as a query parameter. Added a script to index.html that reads the query parameter on load and uses React Router history to restore the correct URL. Both files added to the Vite build output via the public/ directory.
Prevention Pattern
Add the 404.html redirect trick before the first GitHub Pages deployment of any SPA. Test by navigating to a sub-route and refreshing immediately after the first successful deploy. Never ship a React Router app to GitHub Pages without this fix in place.
Recovery
Quick fix (minutes)
Deploy risk
high
Detectable
Immediate on first direct URL visit or refresh
Repeat risk
high
Prevention patterns
Ecosystem impact
Related failures
TrustSeal was deployed to GitHub Pages using the gh-pages npm package, serving from a custom subdomain: trustseal.asquaresolution.com. The GitHub Actions workflow ran cleanly, the build succeeded, and the DNS was configured correctly.
The homepage loaded. Everything appeared fine.
Then a direct navigation to /dashboard was tested — GitHub's standard 404 page. A refresh on the homepage worked. A refresh on any other route: GitHub 404.
This is one of the most common SPA deployment failures and it happens every time a React Router app is deployed to GitHub Pages without handling it upfront.
GitHub Pages does not have a server that can rewrite URLs. When a request comes in for /dashboard, GitHub Pages looks for a file at:
/dashboard/index.html/dashboardNeither exists. The Vite build outputs a single index.html at the root. React Router handles everything client-side — but React Router never gets to run because GitHub Pages returns a 404 before serving any HTML at all.
This is fundamentally different from hosting on a server (like Vercel or a Node.js host) where you can configure a catch-all rewrite: /* → /index.html. GitHub Pages has no such configuration option.
✕GitHub Pages cannot rewrite URLs
There is no nginx config, no _redirects file, and no vercel.json on GitHub Pages. You cannot configure a catch-all route. The 404.html workaround is the only solution available within the GitHub Pages constraint.
GitHub Pages serves 404.html when it cannot find a file for a given path. This is the hook the fix uses.
public/404.html — copy of the redirect script:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
// Preserve the path by encoding it into the query string, then redirect to /
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') + '/' +
'?p=' + encodeURIComponent(l.pathname + l.search) +
(l.hash ? '&h=' + encodeURIComponent(l.hash) : '')
);
</script>
</head>
<body></body>
</html>
index.html — path restoration script (placed before the Vite bundle script tag):
<script>
// Read the path that 404.html encoded into the query string and restore it
(function() {
var params = new URLSearchParams(window.location.search);
var p = params.get('p');
var h = params.get('h') || '';
if (p) {
window.history.replaceState(null, null, p + h);
}
})();
</script>
How it works:
trustseal.asquaresolution.com/dashboard404.html404.html script redirects to /?p=%2Fdashboardindex.html loads, the restoration script runs, rewrites the URL back to /dashboard using history.replaceState/dashboard from the URL, renders the correct componentThe user sees the correct page. The URL bar shows the correct path. No visible redirect.
For deployments to a custom subdomain (trustseal.asquaresolution.com), vite.config.ts must have:
export default defineConfig({
base: '/',
// ...
})
Using a subdirectory base (e.g., base: '/trustseal/') is only needed when deploying to a path like username.github.io/trustseal. On a custom subdomain, the base is the root. Getting this wrong causes all asset references in the built HTML to point to the wrong paths.
This failure is entirely preventable with one rule: ship the 404.html before the first GitHub Pages deployment, not after the first 404.
public/404.html with the redirect script to the project before any deploymentindex.htmlFor any new React Router app targeting GitHub Pages: treat the 404.html as part of the base template, not an afterthought. It takes five minutes to add and saves the debugging cycle every time.
Fix Confidence
Recovery Complexity
Demonstrated In
This failure occurred in a real production context. These case studies show the full arc from incident to resolution.