From 07eb50e79bc9d1ecf62a061525f880d74d268a9d Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Wed, 28 Oct 2026 22:22:53 -0400 Subject: [PATCH] perf(build): serve hashed assets from /_app with immutable caching --- DEPLOY.md | 53 +++++++- public/.nojekyll | 0 public/_headers | 3 + scripts/waf-smoke-test.sh | 11 +- src/worker/index.ts | 20 ++- tests/worker/static-assets.test.ts | 192 +++++++++++++++++++++++++++++ vite.config.ts | 7 ++ 7 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 public/.nojekyll create mode 100644 tests/worker/static-assets.test.ts diff --git a/DEPLOY.md b/DEPLOY.md index 302da456..ee3c29b0 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -15,7 +15,9 @@ against `api.github.com` with no server-side component. **Host the `dist/` build output on any static platform:** GitHub Pages, Netlify, Vercel, S3 + CloudFront, or any CDN that serves SPAs with `index.html` fallback for client-side -routing. +routing. Note: `public/.nojekyll` is copied into `dist/client/.nojekyll` during `pnpm run build` +— keep this file when deploying to GitHub Pages so Jekyll does not strip the `_app/` directory +containing hashed JS and CSS bundles. **What works without a backend:** - All dashboard features (Issues, PRs, Actions tabs) @@ -214,6 +216,20 @@ The OAuth App access token is a permanent credential (no expiry). It is stored i The two tunnel endpoints (`/api/error-reporting` and `/api/csp-report`) receive untrusted browser data and forward it to Sentry. They are hardened with layered fail-fast guards (IP rate limit → Origin check → DSN validation). See `hack/docs/security-runbook.md` for the full threat model, per-endpoint rate limit values, Origin check behavior, CSP field sanitization details, Content-Length pre-check semantics, and fan-out amplification analysis. +### Static asset caching + +- **Hashed bundles (`/_app/*`):** Vite outputs content-hashed JS and CSS bundles to `dist/client/_app/`. `public/_headers` configures `Cache-Control: public, max-age=31536000, immutable` (one year) on `/_app/*` while inheriting the global security headers from `/*`. +- **Entry HTML and unhashed public assets (`index.html`, `/assets/*`):** `index.html` and unhashed images under `public/assets/*` (favicon, logos, social preview) have no custom `Cache-Control` rule in `public/_headers` and use the Cloudflare Workers Static Assets platform default (`Cache-Control: public, max-age=0, must-revalidate`), ensuring browsers revalidate `index.html` on every load. +- **One-time cache miss on initial `/_app/` rollout:** Moving hashed bundles from `/assets/` to `/_app/` causes a one-time cache miss on the deployment that introduces `/_app/`. +- **Stale-chunk 404 handling (`stale_asset_404`):** Under SPA `not_found_handling`, missing non-navigation requests (such as `