The CMS Problem
Every Next.js template project eventually hits the same question: which CMS should we use? Choose Sanity and you are locked into its data model and pricing. Choose WordPress and you inherit its complexity. Choose Shopify and e-commerce is baked in but content management is limited.
The CMS-agnostic architecture solves this by decoupling your frontend from any specific content backend. Your template works perfectly as a static site with no CMS at all — and you can wire in any CMS later without rewriting your components.
Why CMS-Agnostic Matters
In a traditional CMS-coupled template, the data fetching is baked into the components:
// Tightly coupled — cannot swap CMS without rewriting components
const products = await sanityClient.fetch('*[_type == "product"]')This creates an invisible dependency. The moment you choose Sanity, every component that fetches data knows about Sanity. Switching to Shopify means rewriting every component.
With a CMS-agnostic architecture, your components never know where the data comes from:
// Decoupled — component works with any data source
const products = await getProducts()The `getProducts` function is defined in a single data adapter file. Swapping CMS means changing one import in one file, not rewriting 50 components.
The Data Adapter Pattern
The pattern has three layers:
Layer 1: Your components consume typed data via functions. They have no knowledge of where the data originates.
Layer 2: The data adapter (`src/lib/data-adapter.ts`) re-exports the active adapter's functions:
// Swap this one line to change your entire data source
export { getProducts, getProduct, getBlogPosts } from './adapters/static'
// export { getProducts, getProduct } from './adapters/shopify'
// export { getBlogPosts } from './adapters/sanity'Layer 3: The adapters implement the data fetching logic for each CMS. Each adapter exports the same function signatures, so they are interchangeable.
How to Connect WordPress Headless
WordPress with the REST API is the most common "I already have content in WordPress" scenario. The adapter fetches from the REST API endpoint:
// src/lib/adapters/wordpress.ts
const WP_API = process.env.WORDPRESS_API_URL
export async function getBlogPosts(): Promise<BlogPost[]> {
const res = await fetch(`${WP_API}/wp/v2/posts?_embed`, {
next: { revalidate: 3600 }, // ISR: revalidate every hour
})
const posts = await res.json()
return posts.map(transformWordPressPost)
}The `transformWordPressPost` function maps WordPress's field names to your TypeScript types. Your components never see the WordPress response structure — they always receive your typed `BlogPost` objects.
How to Connect Shopify Storefront API
For e-commerce templates, the Shopify Storefront API provides product data via GraphQL:
// src/lib/adapters/shopify.ts
export async function getProducts(): Promise<Product[]> {
const response = await fetch(
`https://${process.env.SHOPIFY_STORE_DOMAIN}/api/2024-01/graphql.json`,
{
method: 'POST',
headers: {
'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_ACCESS_TOKEN!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: PRODUCTS_QUERY }),
}
)
const { data } = await response.json()
return data.products.edges.map(({ node }) => transformShopifyProduct(node))
}The template starts with static product data. When the client is ready to connect their Shopify store, you change one line in `data-adapter.ts` and add two environment variables.
How to Connect Sanity CMS
Sanity uses GROQ (its own query language) via the `@sanity/client` package:
// src/lib/adapters/sanity.ts
import { createClient } from '@sanity/client'
const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2024-01-01',
useCdn: true,
})
export async function getBlogPosts(): Promise<BlogPost[]> {
const posts = await client.fetch('*[_type == "post"] | order(publishedAt desc)')
return posts.map(transformSanityPost)
}Sanity is ideal for content-heavy sites where non-technical editors need a visual editing interface. The real-time preview feature in Sanity Studio is unmatched for marketing teams.
Static JSON: The Default That Works for Everyone
The default adapter reads from JSON files in `src/data/`. This means:
- Zero cost — no CMS subscription
- Zero setup — no accounts, no API keys, no environment variables
- Version controlled — content changes are tracked in git
- Instant — reading from the filesystem is faster than any API call
For most template buyers, static JSON is all they ever need. The CMS adapters are there when content volume grows beyond what comfortable to manage in files.
Making the Switch: Zero Friction
The beauty of this architecture is how effortlessly you can upgrade. A client starts with static JSON. Six months later, their team wants a visual editor. You:
1. Add `@sanity/client` to dependencies
2. Create `src/lib/adapters/sanity.ts`
3. Change one import in `src/lib/data-adapter.ts`
4. Add two environment variables
No component rewrites. No data model changes. No downtime.
Conclusion
CMS-agnostic architecture is not complexity for its own sake — it is a deliberate design choice that gives you and your clients maximum flexibility with minimum lock-in. Every template on nlynx.digital implements this pattern. You get a site that works on day one with zero dependencies, and the freedom to connect any CMS when you need to scale.