BER runs on a small stack I put together deliberately, built for editorial publishing and not for a generic web app. This is what's here, and why I made the choices I did.
Decision 1
Server-first, not client-first.
I spent time trying to build parts of this with client-side fetching. It wasn't worth it for a content site. React Server Components means the HTML arrives already populated: no spinners, no client-side Sanity calls, no waterfalls. The server does the work. The browser just renders what it gets.
Decision 2
Headless CMS, not a custom database.
I thought about building article storage on top of a database. Then I thought about what that actually means in practice. Writing migrations every time a field changes, managing schema evolution, query writing. Sanity already handles all of that. Rich text, image management, content relationships, no migration files.
Decision 3
Cache at the query, not the page.
Page-level caching is too blunt. If I publish something, I shouldn't need a redeploy for it to go live. With unstable_cache on each GROQ query at a 300-second TTL, content propagates in under five minutes, and the queries that rarely change stay cached much longer. Fast site without going fully static.
v16.2.5
App Router throughout, with React Server Components on every page. Each route handles its own data fetching server-side: no prop drilling, no context providers for server data, no client-side waterfalls.
ISR handles page-level caching. A new article goes live within five minutes of being published. No rebuild, no redeploy needed. generateStaticParams pre-renders every known article slug at build time, so the first request to any article hits a cache.
Client Components are narrow: the sticky header, the search modal, the share bar. That's basically it. Everything else never ships a client-side data fetch.
v5.24
Sanity manages all the editorial content: articles, authors, categories. The schema is minimal: only the fields that actually matter for publishing. All content management goes through a custom admin panel at /admin, built on TipTap rather than Sanity's default Studio.
All reads are GROQ queries, co-located with their fetch wrappers in src/sanity/queries.ts and each cached individually via unstable_cache. The point is that Sanity's API gets called as infrequently as possible.
Images go through Sanity's CDN. One small helper function appends width, format, and quality to every image URL at the point of use. No separate image pipeline, nothing to maintain at build time.
Amplify hosts the SSR app. Lambda functions handle dynamic routes; static assets go through CloudFront. The build pipeline is push-to-deploy: git push main triggers a full build and deploy, no CI configuration needed beyond amplify.yml.
One thing that took a while to figure out: Amplify SSM Secrets don't reliably reach the Lambda runtime at build time. The fix is writing all environment variables into .env.production during the prebuild phase in amplify.yml. After that, Next.js picks them up correctly. Took longer than it should have to work out.
Serif headings, navy and teal. No gradients, no decoration. The goal was something that reads like a publication, not a startup.
#1a1a2e
Navy
#006f74
Teal
#cc0000
Red
#FFD700
Gold
#f9f9f7
Surface
#e5e7eb
Border
Display / Headings
Ag
Recoleta
cdnfonts.com · @import in globals.css
Long-form / Article body
Ag
Fraunces
Google Fonts · next/font/google
UI / Labels / Body
Ag
DM Sans
Google Fonts · next/font/google
Pinned versions