Rebuilding dannyshay.com: from Sanity to a stack I own

2 min read

The old version of this site died of a free-tier quota: it was a Next.js frontend on Netlify reading everything from Sanity's hosted CMS, and when the monthly API allowance ran out, the whole site started returning 500s. No content changes, no deploys — just an external meter I didn't control deciding my personal site was done for the month. That was the push to rebuild on a stack I own end to end.

The new stack

  • Next.js (App Router) on Vercel, with incremental static regeneration for every public page — pages are static until content changes, then rebuild on demand via cache tags.
  • Neon Postgres with Prisma as the single source of truth for all content: posts, projects, gallery albums, pages, and site config.
  • Vercel Blob for media, addressed by content hash — every image URL embeds a SHA-256 of the source file, so re-running any pipeline is naturally idempotent (HEAD before PUT, skip what exists).
  • A custom admin CMS (NextAuth credentials + bcrypt, create-only seeding, per-tag ISR revalidation on publish) so editing works from any browser.
  • Self-hosted Jenkins for CI — unit tests, typecheck, a content build gate, and Playwright E2E stages.

Migrating 63 documents and 44 images

The migration ran as a gated pipeline: export the raw Sanity dataset (anonymously — the raw perspective needs no token, which decoupled the migration from the quota that killed the site), transform documents into typed rows, re-host every image as display + thumbnail derivatives in Blob, and reconcile counts at every stage. The gates hard-fail on any mismatch rather than shipping silently incomplete content — and they earned their keep by catching real bugs before any of them reached production.

What actually bit us

Three classes of bugs, all invisible to unit tests with fixture data:

  1. Typed casts that lie. A GROQ query result cast to a TypeScript type is a promise, not a check — three queries projected shapes that didn't match their consumers, and only the live run caught them.
  2. Caches that serialize. Next's unstable_cache round-trips values through JSON, so Date fields silently become strings on warm hits. First render fine, every render after that a crash — now fixed at the loader boundary with regression tests.
  3. Security headers vs. reality. The Content-Security-Policy allowlisted a media domain that was planned but never provisioned, blocking every image on the site. Screenshots caught it in minutes.

Built with AI agents

Most of this rebuild wasn't typed by me. The migration pipeline, the design pass, the CMS wiring, even the bug hunts were done by AI coding agents working under my direction, with the reconciliation gates, tests, and CI there to catch what they got wrong. I write the specs and review the diffs; the agents write most of the code. It's how I build nearly everything now, and it's the only reason a from-scratch rebuild fit into spare evenings.

What's next

DNS cutover from the old Netlify site, and then the fun part: actually writing here again.