From 204197b8b453091850bd16673285009a9795ad79 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Thu, 30 Jul 2026 14:36:01 +0800 Subject: [PATCH] feat(routing): add 404 page and update redirects for proper asset handling --- apps/ui-server-auth/README.md | 2 +- apps/ui-server-auth/public/404.html | 13 ++++++++++ apps/ui-server-auth/public/_redirects | 6 +++-- .../src/cloudflare-pages-routing.test.ts | 26 +++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 apps/ui-server-auth/public/404.html create mode 100644 apps/ui-server-auth/src/cloudflare-pages-routing.test.ts diff --git a/apps/ui-server-auth/README.md b/apps/ui-server-auth/README.md index 3a4d1a026..b65769126 100644 --- a/apps/ui-server-auth/README.md +++ b/apps/ui-server-auth/README.md @@ -23,7 +23,7 @@ pnpm -F @proj-airi/ui-server-auth build ## Deployment -`pnpm -F @proj-airi/ui-server-auth build` writes to `apps/ui-server-auth/dist`. Vue Router owns `/ui/*`, while Vite assets are served from root `/assets/*` so Cloudflare Pages can serve static files without rewriting nested asset paths. Cloudflare Pages uses `public/_redirects` to route `/ui/*` back to the SPA HTML. +`pnpm -F @proj-airi/ui-server-auth build` writes to `apps/ui-server-auth/dist`. Vue Router owns `/ui/*`, while Vite assets are served from root `/assets/*` so Cloudflare Pages can serve static files without rewriting nested asset paths. `public/_redirects` scopes the SPA rewrite to `/ui/*`, and the top-level `public/404.html` keeps missing assets and other unknown paths as HTTP 404 responses. The production GitHub Actions workflow deploys this app to the Cloudflare Pages project `moeru-ai-airi-auth` with separate auth-account credentials: diff --git a/apps/ui-server-auth/public/404.html b/apps/ui-server-auth/public/404.html new file mode 100644 index 000000000..d63aff67c --- /dev/null +++ b/apps/ui-server-auth/public/404.html @@ -0,0 +1,13 @@ + + + + + + Page not found + + +
+

Page not found

+
+ + diff --git a/apps/ui-server-auth/public/_redirects b/apps/ui-server-auth/public/_redirects index e0706e420..1cb8f20dd 100644 --- a/apps/ui-server-auth/public/_redirects +++ b/apps/ui-server-auth/public/_redirects @@ -1,5 +1,7 @@ / /ui/profile 302 /auth /ui/ 302 /auth/* /ui/:splat 302 -/ui /index.html 200 -/ui/* /index.html 200 +# Pages canonicalizes explicit /index.html requests. Proxy UI routes to the +# root asset so the browser keeps the original client-side route. +/ui / 200 +/ui/* / 200 diff --git a/apps/ui-server-auth/src/cloudflare-pages-routing.test.ts b/apps/ui-server-auth/src/cloudflare-pages-routing.test.ts new file mode 100644 index 000000000..d72b3801f --- /dev/null +++ b/apps/ui-server-auth/src/cloudflare-pages-routing.test.ts @@ -0,0 +1,26 @@ +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +import { describe, expect, it } from 'vitest' + +const publicDirectory = resolve(import.meta.dirname, '..', 'public') + +describe('cloudflare Pages routing', () => { + it('keeps missing static assets as 404 responses while preserving the /ui SPA fallback', async () => { + // ROOT CAUSE: + // + // Without a top-level 404.html, Cloudflare Pages treats every missing file + // as an SPA navigation and returns index.html with 200. Missing hashed + // assets are then cached as HTML under their immutable asset URLs. + // + // We keep the SPA rewrite scoped to /ui/* and provide the top-level 404 + // document that makes every other missing file return HTTP 404. + const [notFoundPage, redirects] = await Promise.all([ + readFile(resolve(publicDirectory, '404.html'), 'utf8'), + readFile(resolve(publicDirectory, '_redirects'), 'utf8'), + ]) + + expect(notFoundPage).toContain('Page not found') + expect(redirects).toContain('/ui/* / 200') + }) +})