From c8b541f43b26896fe65ff2a2ddc1c2bdfd70c1e6 Mon Sep 17 00:00:00 2001 From: bhavik-mangla Date: Sun, 30 Aug 2026 10:40:15 +0530 Subject: [PATCH] fix: serve SPA shell for deep links on GitHub Pages GitHub Pages has no rewrite rule for client-side routes, so loading or reloading a route such as /overview or /contributors returned the default "File not found" page instead of the app. Emit 404.html as a copy of index.html at build time. Pages serves it for any unmatched path, the app boots, and React Router resolves the route from window.location with the URL preserved. --- vite.config.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/vite.config.js b/vite.config.js index 1497d5b..41dd477 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,9 +3,39 @@ import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import { vitest } from "vitest"; import svgr from "vite-plugin-svgr"; +import { copyFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; + +/** + * GitHub Pages serves static files only — it has no rewrite rule to map client-side + * routes back to the app shell. So a direct visit to (or a reload of) a route such as + * /overview or /contributors requests a file that does not exist on disk and Pages + * returns its default "File not found" page instead of the app. + * + * Emitting 404.html as a copy of index.html is the documented workaround: Pages serves + * that file for any unmatched path, the app boots, and React Router resolves the route + * from window.location. The URL is preserved, so there is no redirect and no flash. + */ +function spaDeepLinkFallback() { + let outDir; + + return { + name: "spa-deep-link-fallback", + apply: "build", + configResolved(config) { + outDir = resolve(config.root, config.build.outDir); + }, + closeBundle() { + const indexHtml = resolve(outDir, "index.html"); + if (existsSync(indexHtml)) { + copyFileSync(indexHtml, resolve(outDir, "404.html")); + } + }, + }; +} export default defineConfig(({ mode }) => ({ - plugins: [react(), tailwindcss(), svgr()], + plugins: [react(), tailwindcss(), svgr(), spaDeepLinkFallback()], base: "/", test: { globals: true,