Skip to content

feat(renderer): status pages before Astro (RFC-0043 PR 1) - #162

Merged
Madraka merged 3 commits into
mainfrom
feat/status-pages
Nov 8, 2026
Merged

Madraka merged 3 commits into
mainfrom
feat/status-pages

Conversation

@Madraka

@Madraka Madraka commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

What

Rollout PR 1 of RFC-0043 (status pages and maintenance mode), from #117: a site that cannot show its pages answers with one small, translated, self-contained page instead of plain text. Self-contained: no contract, no writer, no maintenance yet.

  • @cloudhal/astro-renderer/status-page: the pure statusPage(state, input) → Response (statusPlain for machine paths, statusPageHtml for hooks) with the closed state set not-published (503, Retry-After: 3600, noindex), maintenance (503, Retry-After to the end), unavailable (503, 300), error (500, reference id only) and preview-expired (410, noindex). Every response is no-store on browser and edge with x-cloudhal-state and the page's own CSP (default-src 'none'); the document stays under 8 KB in every state and language and uses logical properties only. maintenance and preview-expired may carry the published site's typed theme tokens (brand, surface/text when they contrast, the font pairing).
  • Served before Astro in the Site Worker entry (siteState in runtime-workerd.ts, memoised per request so Astro's getSource reads the pointer once), with GET /__cloudhal/health answering { "ok": true, "state": "published" | "not-published" } (the admin's /api/health shape plus state), a top-level catch and Astro 5xx → the error page. Status pages go through the existing render events.
  • Language: host or path-prefix locale, then the site default, then English; RTL from the registry, Western digits for Arabic and Persian dates. The catalog (src/status-strings/) covers every registered Studio language: en.ts is hand-edited, the other 18 come from pnpm i18n:import (the exchange script gains a second source, status.json per locale, same hash guard), and catalog.ts is generated from the registry. Messages use simple ICU arguments only, so no compiler ships in the Worker. The 18 translations are AI drafts awaiting review, as Studio's.
  • Theme hook: an optional statusPage export in src/theme.ts (StatusPageHook from @cloudhal/astro-renderer/status-page) returns a document only; it falls back to the built-in page on undefined, a throw, a non-string, > 64 KB or > 50 ms (logged once per state) and never runs for error. Loaded lazily through virtual:cloudhal/theme-hooks (import("/src/theme.ts")), so old theme copies get the built-in page.
  • cloudhal dev draws each state on demand at /__cloudhal/status/<state> (?locale, ?page=coming-soon, ?message, ?until) through the same module and the project's hook.
  • Starter: the blog's routes use statusPage/statusPlain as their fallback instead of the plain 503s.
  • Behaviour change (changeset): an invalid or expired preview link answers 410 instead of silently showing the published page.

Not in this PR (RFC rollout PR 2): cloudhal build --published writing the status page as the static output for the Docker self-host path, the Caddyfile 503 block and the self-host guide.

Tests run (only the touched files, per the owner's machine rule): packages/astro-renderer/test/status-page.test.ts (29), packages/cli/test/cloudflare-status.test.ts (5, workerd: the page before the first publish with the theme hook, machine paths, health, preview-expired, the publish flip), cloudflare-multisite.test.ts (5, with the new two-host check: an unpublished site on its own host gets its page with no-store and Vary: Host, its siblings serve), cloudflare.test.ts + packaging.test.ts (20), dev-status.test.ts (1), apps/studio/test/i18n-messages.test.ts (9). pnpm lint, pnpm typecheck, pnpm size:check, pnpm design:lint and node scripts/i18n-exchange.ts check green.

Checklist

  • pnpm check and cargo test --workspace pass locally (not run: only the touched test files, lint, typecheck, size and design lint; CI runs the rest)
  • Contract change? pnpm gen was run and generated files are committed (docs/adr/0001) — no contract change
  • User-visible change in a published package? pnpm changeset added
  • Commits are signed off (git commit -s, DCO)
  • Docs / AGENTS.md updated if behaviour or commands changed

🤖 Generated with Claude Code

A site that cannot show its pages answers with one small, translated,
self-contained page instead of plain text: not-published (503,
Retry-After 3600, noindex), preview-expired (410), unavailable (503) and
error (500, reference id only), every one no-store with its own CSP and
under 8 KB. The Site Worker entry decides the state before Astro runs
(siteState, memoised per request), answers GET /__cloudhal/health with
{ ok, state } in the admin's shape, and turns any failure into the error
page. The catalog covers every registered Studio language through the
i18n exchange flow (a second source of scripts/i18n-exchange.ts; only
en.ts is hand-edited) with simple ICU arguments only. A theme may export
statusPage from src/theme.ts, loaded lazily through
virtual:cloudhal/theme-hooks and falling back to the built-in page on a
throw, a slow answer or an oversized document. cloudhal dev draws each
state at /__cloudhal/status/<state>. The blog starter's routes use the
renderer's fallbacks. An invalid preview link now answers 410 instead of
the published page.

Refs #117

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
The deploy test expected the runtime's default 500 for a site Worker
whose site row is not pushed yet; the entry now answers 503 with the
`unavailable` status page (Retry-After 300, no-store), and the pushed
but never published site answers the `not-published` page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
CodeQL (js/polynomial-redos) flagged the trailing-slash regex in the
status page's locale choice: it runs on request text. A linear loop
replaces it, with a unit case of 200,000 slashes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
@Madraka
Madraka merged commit 7b61d6f into main Nov 8, 2026
9 checks passed
@Madraka
Madraka deleted the feat/status-pages branch November 8, 2026 05:04
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.

2 participants