Open Source
Your components don't know
which CMS is running.
agnostik is a React Router starter built around one idea: your UI should have no opinion about where its data comes from. Swap CMS sources with one env var. Components never change.
What you get
No opinions — by design
Features
No commitment to your backend. Or your host. Or your CMS.
One env var. Any CMS.
Swap from Sanity to PocketBase — or bring your own adapter — by changing CMS_SOURCE and restarting. Zero component changes.
SSR on every request
React Router v7 in framework mode. Data loads server-side — no loading spinners, no client waterfalls, no hydration mismatches.
Design system built in
Tailwind v4 with dark mode, compact mode, accent colors, and radius scale — all toggled via html class flags with no JavaScript overhead.
Components that don't know
Three layers: primitives → blocks → sections. Components receive normalised data and render it. They have no knowledge of — and no opinion about — the source.
Get started in minutes
Clone the repo, install dependencies, and copy the example env file:
git clone https://github.com/your-org/react-router-boilerplate my-site
cd my-site && npm install
cp .env.example .env
Set CMS_SOURCE=stub in .env to run with no external dependencies — no API keys, no credentials, no setup required:
npm run dev
The dev server starts at http://localhost:5173. All data comes from stub.json until you connect a live CMS.
Architecture
Under the hood
SSR
Rendering strategy
Every page load goes server-side. No stale client cache, no rehydration flash.
4
Steps to a new section
Type → Component → Register → Data. The renderer handles the rest.
DataSource interface
Two methods — getPage and listPages. That's the entire contract. The rest of the app is genuinely agnostic: it never knows, and never needs to know, which backend it's talking to.
Section registry
One entry maps a componentType string to a React component. Add it once and it's immediately available in the renderer, stories, and documentation.
3
Component layers
Primitives, blocks, and sections with hard boundaries. Editors interact only with sections.
1
Catch-all route
$.tsx handles every CMS-driven page. No new route files needed for new content.
Philosophy
Noncommittal by design
Most starters commit early. agnostik commits to as little as possible — so you can commit to whatever you need.
The name comes from the Greek agnōstos — "not known." That's literally what's happening: your components receive normalised data and render it. They have no knowledge of, and no opinion about, which CMS produced it. Swap the source and they keep working. They never knew.
What agnostik refuses to commit to
- Your CMS — Sanity, PocketBase, or bring your own. Implement two methods, set an env var, and the rest of the codebase never changes.
- Your content model — Components define their prop contracts. CMS schemas follow those contracts — not the other way around.
- Your design system — CSS tokens, not component logic. Dark mode, compact mode, accent colors — all toggled by a class on the html element.
- Your host — SSR on any Node server. Fly.io, Railway, Render, bare metal. No platform-specific adapter needed.