Next.js 16 · Sanity 5 · JavaScript
The boring decisions, already made.
A Next.js and Sanity starter with the plumbing wired and the sharp edges filed off. Page builder, fetch layer, draft mode, a signed revalidation webhook, a responsive system anchored to your Figma frames, and a Claude Code toolkit that cannot drift out of sync with your schemas.
€69 one-time · one developer, unlimited projects
- next
- 16.2
- react
- 19.2
- sanity
- 5.31
- tailwind
- 3.4
- blocks
- 14
- guards
- 4
- skills
- 5
- drift
- 0
- NEXT 16.2
- REACT 19.2
- SANITY 5.31
- TAILWIND 3.4
- JAVASCRIPT
- 14 BLOCKS
- 4 BUILD GUARDS
- DRIFT: 0
This is a content architecture, not a website. It decides how content is modelled, how it is fetched, where secrets live, how a design maps to CSS, and what happens when an editor renames a block. The libraries on top are defaults you can swap.
It is deliberately small. No TypeScript, no typegen, no MCP servers, no multi-language. If you want those, there is a more complete product and this page tells you where to find it.
001 A secret with a NEXT_PUBLIC_ prefix002 An empty dataset returning 500 on every route003 Links resolved in the browser004 A block in the schema but not in the renderer005 Fluid type from a visual generator006 Renaming a block007 AI instructions describing last year's schema008 A layout that loads every block client-side009 Typography plugins at a 10px root010 One Open Graph card for the whole site011 A sitemap generated at build time012 A config option that never existed
Twelve ways a Sanity setup quietly breaks
Every one of these was in this repository, and every one is now a fixed bug, a guard that fails the build, or both. Each fix is in the code you get, with the guard that stops it coming back — this is not a list of things that happen to other people.
A secret with a NEXT_PUBLIC_ prefix
Next inlines those into every bundle that touches the module. The value ships to every visitor with no warning, no error, and no visible difference in development.
until someone greps your bundle
An empty dataset returning 500 on every route
A missing singleton makes the root layout's metadata throw, and a throw there takes the whole site with it. It only happens on a brand-new project — which is to say, on every buyer's first run.
your first five minutes
Links resolved in the browser
A component that turns a _ref into a path inside useEffect renders no anchor on the server. Crawlers see a page with no internal links at all, and every link costs a round-trip.
every page, every crawl
A block in the schema but not in the renderer
It is selectable in the Studio, an editor places it on a page, and it renders nothing. No error in production — just a gap where their content should be.
two blocks, several months
Fluid type from a visual generator
Stacked max-width media queries with independently calculated values do not meet at their boundaries. The page jumps backwards as the window gets wider, and the more breakpoints you add the worse it gets.
3–4 hours to even find
Renaming a block
Sanity does not validate published documents against the current schema. The old block stays in every document that used it and renders nothing, forever.
silent, and permanent
AI instructions describing last year's schema
Nothing checks them. An agent reads them as fact and generates code against types that were deleted months ago — confidently, and at speed.
2,700 lines, in this repo
A layout that loads every block client-side
One `use client` at the top of a shared wrapper, for an animation prop nothing passes, and the entire page builder crosses the boundary with it.
all five blocks, invisibly
Typography plugins at a 10px root
A 62.5% root makes Figma maths trivial and makes `prose` render a 10px article, because it hardcodes 1rem and sizes every descendant in em off that.
one confusing afternoon
One Open Graph card for the whole site
The App Router merges metadata field by field but replaces openGraph wholesale. A page that sets only a title inherits the layout's entire block, title included.
every share, same preview
A sitemap generated at build time
Publish an article and it appears on the site through revalidation — and in the sitemap at the next deploy, whenever that happens to be.
days of missed indexing
A config option that never existed
Smooth-scroll libraries ignore keys they do not recognise. The option had been misspelled since the day it was written, so the behaviour was accidental and the code asserted the opposite.
nobody notices, ever
already decided
Fourteen decisions you do not have to make
Not features — decisions. Each one is a fork you would otherwise stand at for an afternoon, and several are enforced by a script rather than left as advice.
001Server Components fetch
Content arrives as a `data` prop from a Server Component. No loading state to design, no waterfall, no fetch layer shipped to the browser.
002References resolve in GROQ
Shared fragments turn a link into a ready href and an image reference into a URL with real dimensions. If a component needs a useEffect to know what to render, the query is missing a projection.
003Secrets cannot reach the browser
Public config and server secrets are separate modules, and the build scans its own client output for credentials afterwards. It fails rather than warns.
004Figma pixels divide by ten
The root is fixed at 62.5%, so 1rem is 10px at every viewport and a 32px value from your design is text-[3.2rem]. No scale to memorise.
005Fluid type without breakpoints
text-fluid-[32,48] is 32px at your mobile frame and 48px at your desktop one, interpolating between. A check runs the curve from 200px to 6000px and fails if it ever goes backwards.
006Publishing reaches the live site at once
A signed webhook expires the cache the moment you publish, and every fetch carries the tag it clears. Visitors with the site open get the change through live updates too.
007Routing comes from content
An editor sets a pathname and the route exists. Every document with one appears in the sitemap automatically, with real lastmod dates.
008The Studio keeps its own root
A 62.5% root would shrink every Sanity UI measurement by 37%. The Studio is a separate root layout with a second lock in CSS.
009Colours are roles, and measured
Components ask for bg-surface because it is a raised surface. Each token carries its contrast ratio, and the ones that cannot carry text are marked decorative rather than left for you to discover.
010Sections invert without a branch
A theme field re-declares the colour properties for one subtree. Every utility inside follows on its own — no component ever learns a dark mode exists.
011A block is four files, all checked
Schema, group, component, registry entry. The build fails if any is missing, in either direction, so a block cannot be insertable and invisible at the same time.
012The AI instructions cannot drift
A guard resolves every schema name in the agent docs against the schemas themselves, and holds the inventories to set equality. Rename a block and the build names every file still mentioning it.
013Images keep their focal point
Hotspot and crop travel from the Studio through to the CDN URL and the object-position, so an art-directed crop survives every breakpoint.
014Animation degrades to content
Every animated element renders complete and correct with JavaScript off. Motion is a layer over something already in the HTML, never the thing that puts it there.
the page builder
A block is four files, and the build checks all four
Editors compose pages from blocks. The registry is a plain object keyed by schema name, so the mapping is readable at a glance — and a script asserts every block has all four pieces, because a block in the schema with no component is selectable and silent.
$ npm run check:blocks[check-blocks] OK — 14 blocks, schema and component in step. # adding one:sanity/schemas/components/Pricing.js the schemasanity/schemas/components/index.js a groupcomponents/Pricing/Pricing.jsx the componentobjects/PageBuilder.jsx the registry entry # or ask for it:$ /new-block pricing table
the fetch layer
Everything resolves in the query, never in a component
Shared GROQ fragments turn references and assets into finished values on the server. This is the difference between a page whose internal links exist in the HTML and one whose links appear a round-trip later, after the crawler has gone.
export const linkFields = groq` _key, label, "href": select( linkType == true => externalLink.href, defined(internalLink.reference->pathname.current) => internalLink.reference->pathname.current, defined(internalLink.reference->_id) => "/", // a singleton null ),`; // the component receives { label, href } and renders an anchor.// it never learns what a _ref is.
the responsive system
Two numbers from Figma, and the type follows
You give it your mobile and desktop frame widths. `text-fluid-[32,48]` is then 32px at the first and 48px at the second, interpolating between with no breakpoint and no scale to learn. Below is that exact utility, resolved.
| Viewport | text-fluid-[32,48] | What it means |
|---|---|---|
| 320px | 32.0px | below the mobile frame — held, not shrunk |
| 390px | 32.0px | the mobile frame. Exactly your Figma value |
| 768px | 36.9px | interpolating |
| 1024px | 40.2px | interpolating |
| 1620px | 48.0px | the desktop frame. Exactly your Figma value |
| 1920px | 51.9px | the cap |
| 2560px | 51.9px | still the cap — it never runs away |
Computed from design.config.js, not measured by hand. `npm run check:fluid` asserts both anchors and walks the curve from 200px to 6000px looking for a step backwards — and it carries a negative test, so a formula that sawtooths fails the check rather than passing quietly.
guard rails
Four things the build refuses to let you ship
Documentation rots and conventions slip. These run on every build, and each one exists because the thing it prevents had already happened here.
$ npm run check [check-fluid] OK — anchors exact, curve monotonic, 200→6000px[check-blocks] OK — 14 blocks, schema and component in step.[check-toolkit] OK — 15 instruction files, every schema reference resolves.✓ Compiled successfully[check-secrets] OK — scanned 119 files, no credentials found. # and when something is wrong:[check-toolkit] FAILED — 4 stale inventory listing(s): CLAUDE.md:111 blocks inventory is out of date no longer exists: Accordion
- app
- (pages)
- (admin)/admin
- api/revalidate
- sitemap.js robots.js
- components
- objects
- sanity
- env.js / env.server.js
- fragments.js
- schemas
- design.config.js
- theme.config.js
- seed
- scripts
- docs
- 1# What you actually get
- 2
- 3Source you unzip and own. No CLI to install, no licence check phoning home, no build step between you and the source.
- 4
- 5## What's inside
- 6
- 7- app/ — routes — public site and Studio, separate root layouts
- 8- (pages)/ — the site. Sets the fluid class
- 9- (admin)/admin/ — Sanity Studio, isolated from the site's styles
- 10- api/revalidate/ — signed publish webhook
- 11- sitemap.js robots.js — generated from Sanity, not from a config file
- 12- components/ — fourteen page-builder blocks, plus header and footer
- 13- objects/ — shared UI — Container, Figure, RichText, PageBuilder
- 14- sanity/ — client, live, queries, fragments, structure
- 15- env.js / env.server.js — the public/secret split, enforced by the build
- 16- fragments.js — link, image, video and rich text projections
- 17- schemas/ — documents, blocks, field objects, singletons
- 18- design.config.js — the four numbers the responsive system derives from
- 19- theme.config.js — the palette, both themes, single source of truth
- 20- seed/ — this site, as importable content
- 21- scripts/ — setup wizard and the four build guards
- 22- docs/ — eleven documents, one per subsystem
- .claude/skills
- sanity-schema
- sanity-block
- sanity-object
- sanity-document
- sanity-content-model
- optional
- .claude/commands
- CLAUDE.md
- AGENTS.md
- scripts/check-toolkit.js
- 1# Instructions an agent cannot outgrow
- 2
- 3Skills load on their own when your request matches one; the commands are there for when you already know what you want. Both point at the same procedures, and a guard keeps those true to the schemas they describe.
- 4
- 5## What's inside
- 6
- 7- .claude/skills/ — what Claude reads — the substance
- 8- sanity-schema/ — field types, GROQ, verified icon names
- 9- sanity-block/ — add a block — all four files, in order
- 10- sanity-object/ — add a reusable field object, and register it
- 11- sanity-document/ — document type with routing, query and route
- 12- sanity-content-model/ — plan an architecture before writing schemas
- 13- optional/ — Linear ticket → Figma frame → block. Off until moved
- 14- .claude/commands/ — /new-block · /new-object · /new-document
- 15- CLAUDE.md — loaded every session — rules only, no explanation
- 16- AGENTS.md — points Cursor, Copilot and Codex at the same rules
- 17- scripts/check-toolkit.js — fails the build if the docs name a schema that is gone
scope
What this is not
Written down so you can decide before paying rather than after. None of these are coming later.
001Not TypeScript
JavaScript throughout, deliberately. If strict types are a requirement, this is not your starter — and that is a better outcome for both of us than a regret in three weeks.
002No typegen or MCP servers
Sanity typegen depends on TypeScript. MCP servers are a different product at a different price, and this page tells you which one.
003Single locale
No i18n. Multi-language is genuinely large and half of it is worse than none of it.
004No component library
No UI kit to theme, no design system to learn. Fourteen blocks that show the conventions, and then it is your code.
005No auth or billing
No dashboard scaffolding, no user accounts, no payments. This is a content architecture.
006Not a no-code builder
You write real code on top of it. There is no drag-and-drop and no visual editor.
honestly
Where this sits
There is a more complete product in this niche. It is good, it costs several times this, and it is aimed at a different buyer. Pretending otherwise would waste your afternoon and my reputation.
| A free starter | This · €69 | The €549 option | |
|---|---|---|---|
| Shows how the pieces connect | yes | yes | yes |
| Decides how you fetch and model | no | yes | yes |
| Guards that fail the build | no | four | yes |
| Agent instructions | no | skills + commands | skills + MCP + llms.txt |
| TypeScript strict | sometimes | no | yes |
| Sanity typegen | no | no | yes |
| Multi-language | no | no | no |
| Award-winning showcase | no | no | yes |
| Price | free | €69 | €549 |
If the right-hand column is what you need, buy that one. This is the smaller, cheaper, honest option — and the rows where it says no are the reason it costs what it costs.
- one price
- one developer
- unlimited projects
- no attribution
- full source
- €69
One price, one developer, forever
Delivered as a ZIP of the full source. Commit it as your project's first commit and keep an untouched starter branch beside it — each update is a new ZIP merged through that branch, so Git shows you exactly what changed. Sold through Gumroad, which handles the payment, the tax and the download.
€69one-time, per developer
every licence includes
- 001Full source, no build step, no licence check
- 002Unlimited projects, personal and commercial
- 003Client work, and the site you deliver is theirs
- 004No attribution, no badge, no backlink
- 005Updates for as long as it is maintained
- 006The full source as a ZIP, re-downloadable from your purchase
- 007The demo is the product — see all of it before you buy
and deliberately does not
- 001TypeScript — this is JavaScript, deliberately
- 002Typegen, MCP servers, llms.txt
- 003Multi-language — single locale by design
- 004A UI kit or component library to theme
- 005Auth, billing or dashboard scaffolding
before you buy