Skip to main content
ngockhoi96
← Roadmap
Shipped v0.3.0 ·

Search & engagement

Instant full-text search (Pagefind), plus anonymous, cookieless view counts and likes backed by D1 + KV — no analytics, no tracking, no accounts.

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.