Skip to content

Resilience: 404/500/503 pages, dynamic web app manifest, X-Redirect-By - #38

Merged
alexpricedev merged 1 commit into
mainfrom
alexpricedev/resilience-section-audit
Jul 14, 2026
Merged

Resilience: 404/500/503 pages, dynamic web app manifest, X-Redirect-By#38
alexpricedev merged 1 commit into
mainfrom
alexpricedev/resilience-section-audit

Conversation

@alexpricedev

Copy link
Copy Markdown
Owner

Implements the Resilience section of specification.website, the next section in the ongoing spec audit (Foundations, SEO, Accessibility, Security, Performance, Privacy already done).

What's in here

Custom error pages — 404 / 500 (Required)

  • New ErrorLayout — same header/nav/footer chrome as the site, but ships zero client JS (no importmap, Lottie, or main bundle) so error pages render instantly even when the app is degraded, and always noindex. Extracted a shared SiteFooter so the repo-specific links live in one place.
  • templates/error.tsx + utils/errors.tsx render render404() / render500() / render503().
  • 404: fallback.ts now returns the styled, navigable page instead of plain-text "Not found" (correct status, homepage link, no leaks).
  • 500: a new handleGuarded() wraps both the routed handlers and the fallback — it catches any uncaught error, logs it server-side, and returns the styled 500 with no stack-trace / path leak. A Bun.serve error() backstop also suppresses Bun's dev error page (which does leak).

503 maintenance (Recommended)

  • MAINTENANCE_MODE=true serves a 503 with a Retry-After header (default 3600s, override via MAINTENANCE_RETRY_AFTER) for every request except /health (so the platform doesn't cycle the instance). Documented in .env.example.

Web app manifest (Recommended)

  • Made dynamic so the name is never stuck as "Billet": deleted the static file, added buildWebManifest() driven by SITE_NAME (+ start_url, scope, display: standalone, theme/bg colours, 192/512 icons, and a maskable icon), served at /site.webmanifest with application/manifest+json.
  • START_PROMPT.md notes that the manifest and X-Redirect-By both follow SITE_NAME — renaming that one constant renames both.

X-Redirect-By (Recommended)

  • Stamped centrally in withSecurityHeaders: any response with a Location header gets X-Redirect-By: <SITE_NAME>. One line covers every redirect — the redirect() helper, the 308 trailing-slash, and the inline 303s in the auth/admin middleware.

Graceful degradation (Recommended)

  • Already passed (full SSR, real <a>/<form action>, the single projects-search island degrades to the server-rendered list). Reinforced by making the new error/maintenance pages zero-JS.

Deferred (Optional / infra)

Service worker / offline (skipped by request); Deprecation/Sunset headers (no versioned endpoints yet); external synthetic monitoring, separate-host status page, and RUM/web-vitals (ops/infra — the /health liveness endpoint already exists).

Testing

  • bun run check clean; 321 tests pass (added errors.test.tsx, webmanifest.test.ts, plus X-Redirect-By and handleGuarded cases; updated fallback.test.ts).
  • Verified live in-browser: 404 (status + nav + CTA navigates home), 500, 503 (correctly drops nav/home button), and the manifest (application/manifest+json, name/short_name/start_url/maskable).

How to test the error pages locally

  • 404: visit any nonexistent URL, e.g. /nope.
  • 503: set MAINTENANCE_MODE=true in .env, restart — every page 503s except /health; unset to lift.
  • 500: temporarily throw new Error("preview 500") in a controller (e.g. home.index()), hit the page, remove it. The real error logs to the terminal, never to the user.

🤖 Generated with Claude Code

Implements the Resilience section of specification.website.

- Custom 404/500/503 pages via a zero-JS ErrorLayout (no importmap/Lottie/
  bundle), noindex, same header/nav/footer chrome. 404 wired into the fallback;
  500 via a new handleGuarded() pipeline (maintenance gate -> try/catch ->
  styled 500 with server-side logging, no stack-trace leak) that both
  secureRoutes and the fetch fallback route through, plus a Bun.serve error()
  backstop.
- 503 maintenance mode: MAINTENANCE_MODE=true serves a 503 + Retry-After for
  all paths except /health (documented in .env.example).
- Web app manifest served dynamically from SITE_NAME (never stuck as "Billet"):
  buildWebManifest() + /site.webmanifest route with application/manifest+json,
  start_url, and a maskable icon. Deleted the static public/site.webmanifest.
- X-Redirect-By stamped centrally in withSecurityHeaders for any redirect
  (redirect() helper, 308 trailing-slash, auth/admin 303s).
- START_PROMPT note: manifest + X-Redirect-By follow SITE_NAME automatically.

bun run check clean; 321 tests pass (added error/manifest/maintenance/
X-Redirect-By coverage). Verified live in-browser (404/500/503/manifest).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alexpricedev
alexpricedev merged commit 6293740 into main Jul 14, 2026
3 checks passed
@alexpricedev
alexpricedev deleted the alexpricedev/resilience-section-audit branch July 14, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant