A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1# SP7 — Deploy (Cloudflare)
2
3- **Date:** 2026-06-08
4- **Status:** Deploy-ready + edge-verified locally. The final `wrangler deploy` runs on the
5 owner's Cloudflare account.
6- **Goal (brief §8, §9.8):** Deploy to a free host. Cloudflare.
7
8## Decision (0009)
9
10Deploy to **Cloudflare Workers (Static Assets)** via `@astrojs/cloudflare` v13 — the
11modern Astro Cloudflare target (Cloudflare is folding Pages into Workers + Static Assets;
12the adapter no longer emits a Pages `_worker.js`). Same Cloudflare account + custom domain
13as "Pages"; the read-through renderer (SP4) runs as the Worker, static assets (landing,
14editor shell, fonts, `client-metadata.json`) served from the edge cache.
15
16- `nodejs_compat` (in `wrangler.toml`) lets `sanitize-html` run on `workerd`.
17- **No database / KV / queues** — the read-through design needs none. Astro's default
18 session store (which would add a KV) is disabled with an in-memory driver.
19- Cloudflare's `fetch` cannot reach private/loopback IPs, which also closes the
20 DNS-rebinding residual noted in Decision 0007 — SSRF is fully contained at the edge.
21
22## Edge verification (local `workerd` via `astro preview`)
23
24- Landing (static asset) → 200.
25- Article `/@jeherve.com/<rkey>` (SSR + `sanitize-html` + reader fetch chain) → 200, renders
26 with both `site.standard` link tags. **`sanitize-html` works under `nodejs_compat`.**
27- `/@127.0.0.1/x` → 404 (SSRF guard holds on the edge).
28- Generated `dist/server/wrangler.json`: `main: entry.mjs`, `assets: ../client`,
29 `compatibility_flags: [nodejs_compat]`, `kv_namespaces: []`.
30
31## Deploy runbook (owner's Cloudflare account)
32
331. **One-time:** `npx wrangler login` (authorise the Cloudflare account).
342. **Deploy:** `npm run deploy` (= `astro build && wrangler deploy`). `astro build` writes
35 `.wrangler/deploy/config.json` redirecting `wrangler deploy` to the built worker config.
363. **Custom domain:** in the dashboard (Workers & Pages → `skypress` → Settings → Domains),
37 attach **`skypress.blog`** (the domain is already on Cloudflare). This routes the apex to
38 the Worker.
394. **Verify production:**
40 - `https://skypress.blog/` (landing), `/preview` (sample article).
41 - `https://skypress.blog/client-metadata.json` returns the OAuth client metadata
42 (`client_id` must equal that URL — it does).
43 - `https://skypress.blog/editor` → sign in (prod OAuth uses the hosted `client_id`;
44 `redirect_uris` is `https://skypress.blog/editor`), publish, then open
45 `https://skypress.blog/@<handle>/<rkey>`.
46
47Git-connected builds (Workers Builds) are an alternative: build `npm run build`, deploy
48`npx wrangler deploy`, set `nodejs_compat` — but the local `npm run deploy` is the simplest
49first cut.
50
51## OAuth client metadata: a worker route, not a static file
52
53`/client-metadata.json` is served by a **worker route**
54(`src/pages/client-metadata.json.ts`, `prerender = false`), not a static asset. Two prod
55failures drove this:
56
571. **404 in production.** The static `public/client-metadata.json` served locally and on
58 the first deploy but 404'd at the deployed origin (`invalid_client_metadata … Not
59 Found`). Static `.json` serving on Cloudflare Workers Static Assets proved unreliable;
60 a worker route always runs.
612. **Trailing slash.** Cloudflare serves the prerendered editor page canonically at
62 **`/editor/`** (`/editor` → 307 → `/editor/`), and the atproto client only processes a
63 callback when `location.pathname` **exactly** equals a registered `redirect_uri`
64 pathname (`findRedirectUrl`). So the metadata must register `…/editor/` **with** the
65 slash, or auth completes at Bluesky but the editor silently stays signed-out.
66
67The route generates the document from the **request origin**, so `client_id` always equals
68the fetched URL (apex / www / preview origins all work) and `redirect_uris` is
69`<origin>/editor/`. Verified on workerd (`astro preview`): 200 `application/json`. Reading
70routes are SSR (not slash-canonicalised), so article URLs stay `/@<handle>/<rkey>`.
71
72## Notes / follow-ups
73
74- The dev OAuth client is loopback (`127.0.0.1`); production uses the hosted
75 `client-metadata.json` automatically (Decision 0004 / `getClientMode`).
76- The editor island is heavy (~1.5 MB gz) but loads only on `/editor`; reading pages stay
77 zero-JS at the edge.
78- A caching layer / `Cache-Control` tuning for the read-through renderer is a perf
79 follow-up (content is immutable per rkey until edited).