diff --git a/docs/operations.md b/docs/operations.md index 143711fbc..2db27e8d8 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -42,10 +42,12 @@ Vercel builds and serves the frontend from the Git repository. Merging to `main` - the Vite build command and `dist` output; - SPA fallback to `index.html`; -- no-cache headers for `index.html`, `theme-init.js`, and `sw.js`, and immutable caching for hashed `/assets/`; +- no-cache headers for `/`, `index.html`, `theme-init.js`, and `sw.js`, and immutable caching for hashed `/assets/`; - `noindex` headers for `/app`, `/login`, and `/share` routes; - Content Security Policy and the other response security headers, including the `Permissions-Policy` that allows the microphone for the app's own origin only. +The build also writes `dist/terms/index.html` and `dist/privacy/index.html`: copies of the app shell whose canonical URL names their own route instead of the landing page (`selfCanonicalShells` in [`vite.config.ts`](../vite.config.ts)). Vercel serves them before the SPA fallback. Add a route there when a new SPA page joins `public/sitemap.xml`. + [`src/test/vercel-deployment-config.test.ts`](../src/test/vercel-deployment-config.test.ts) guards these rules. Update it together with `vercel.json`, and do not paste the full configuration into another guide. A Vercel deploy ships only the frontend. Supabase migrations and Edge Functions are separate operations with their own authorization, so a frontend that depends on new schema must wait until that schema is applied to production. diff --git a/e2e/public-pages.spec.ts b/e2e/public-pages.spec.ts index 63f602f68..b27d3d5c3 100644 --- a/e2e/public-pages.spec.ts +++ b/e2e/public-pages.spec.ts @@ -7,6 +7,8 @@ const PUBLIC_ROUTES = [ { path: '/terms', name: 'terms' }, { path: '/privacy', name: 'privacy' }, { path: '/login', name: 'login' }, + // An unknown route renders the 404 page, which must not log an error. + { path: '/no-such-page', name: 'not-found' }, ] as const; for (const locale of LOCALES) { @@ -25,7 +27,9 @@ for (const locale of LOCALES) { const response = await page.goto(route.path, { waitUntil: 'domcontentloaded' }); expect(response?.ok()).toBeTruthy(); - await expect(page.locator('body')).toBeVisible(); + // Pages are lazy chunks: wait until the page itself has rendered, so + // errors it logs while mounting are in the list below. + await expect(page.locator('h1').first()).toBeVisible(); const fatalErrors = consoleErrors.filter((msg) => !msg.includes('Failed to load resource') && diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index f3e826807..9c2afa324 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -15,16 +15,18 @@ const LandingPage = () => { return (