The dynamic layer turns on — without giving up static-first. Search is static assets; engagement is thin, additive, and degrades gracefully.
What shipped
- ⌘K command-palette search (Pagefind, client-side, no Worker/D1) — accent-insensitive, <mark> excerpts, heading sub-results, navigate-out chips
- View counter — a Server Island reads the durable total from D1 and increments off the response path (waitUntil) with cookieless KV dedup
- Like button — optimistic toggle via the likePost Astro Action, native rate limit (1/min), liked:{slug} cookie for instant state
- Popular block on the homepage — top-N by view_count (Server Island, KV-cached ~5min)
- D1 engagement schema (post_stats + likes) + an Astro-free lib/engagement.ts covered by Miniflare tests
- Go-live (v0.3.1) — provisioned real D1/KV, secrets, remote migrations
The decision that defined it
Storage went to D1 + KV, not Durable Objects (ADR-0013), and the counter total lives in D1 with an atomic increment — fixing a lost-update race a KV counter would have. Identity is a salted, day-rotating hash; the raw IP is never stored.
view → ViewCount island → read total (D1) → render └→ waitUntil: dedup (KV) → atomic +1 (D1) # never blocks paint Reflections
To write: why cookieless + privacy-first, the KV-vs-D1 counter race I’m glad I caught, and what server islands changed about how I think about caching.