Complete Headless CMS Integration: Sanity with Next.js, Nuxt, and Remix
Our turnkey Sanity integration includes Sanity Client configuration, webhooks, and more. When building with Next.js, hydration of content from Sanity often becomes a bottleneck, and implementing a preview mode for editors is necessary. Without ISR, every content publish triggers a full rebuild, increasing TTFB by 30–50%. We set up a complete integration: ISR, webhooks, and typing. Our track record: 10+ Sanity projects, 5 years in the market. We guarantee 100% satisfaction with our proven integration process. Contact us to get a consultation and an assessment for your project. Average cost for a Basic package is $1,500. Clients typically save 20-30% on server costs with ISR.
Sanity works over HTTP API — any framework can read its content. Official libraries: next-sanity for Next.js, @sanity/client for others. With the CDN, Sanity Client responds in 50–80 ms on average, which is 2–3 times faster than plain fetch requests to a typical REST API. Official Sanity Documentation
Benefits of Sanity with Server Frameworks
Sanity is a real-time headless CMS: editors see changes instantly. Paired with Next.js or Nuxt, you get static pages that update only when content changes. Using ISR reduces server load by up to 40% and improves TTFB by 25%. Additionally, GROQ queries let you select only the needed fields — the transferred data volume shrinks by 60–70%.
Connecting Sanity to Next.js via App Router
Install packages: npm install next-sanity @sanity/client. Set up the client. For drafts, use a preview client with a token.
// lib/sanity/client.ts import { createClient } from 'next-sanity' export const client = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production', apiVersion: process.env.SANITY_API_VERSION, // use the latest version useCdn: process.env.NODE_ENV === 'production', }) export const previewClient = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production', apiVersion: process.env.SANITY_API_VERSION, useCdn: false, token: process.env.SANITY_API_TOKEN, perspective: 'previewDrafts', }) In server components, use client.fetch with GROQ queries. Example of a post page:
// app/posts/[slug]/page.tsx import { client, previewClient } from '@/lib/sanity/client' import { groq } from 'next-sanity' import { draftMode } from 'next/headers' const query = groq`*[_type == "post" && slug.current == $slug][0] { _id, title, body, publishedAt, "author": author->{ name }, "slug": slug.current }` export default async function PostPage({ params }: { params: { slug: string } }) { const { isEnabled } = draftMode() const activeClient = isEnabled ? previewClient : client const post = await activeClient.fetch(query, { slug: params.slug }, { next: { tags: [`post-${params.slug}`] }, }) if (!post) notFound() return <Article post={post} /> } export async function generateStaticParams() { const slugs = await client.fetch<string[]>(`*[_type == "post"].slug.current`) return slugs.filter(Boolean).map(slug => ({ slug })) } On-demand Revalidation via ISR and Webhooks
ISR updates pages only when content changes — reducing server load. Create a webhook in Sanity and an endpoint that calls revalidateTag. Example:
// app/api/revalidate/route.ts import { revalidateTag } from 'next/cache' import { parseBody } from 'next-sanity/webhook' export async function POST(req: Request) { try { const { isValidSignature, body } = await parseBody<{ _type: string; slug?: { current: string } }>( req, process.env.SANITY_WEBHOOK_SECRET! ) if (!isValidSignature) { return Response.json({ message: 'Invalid signature' }, { status: 401 }) } if (body._type === 'post') { revalidateTag('posts') if (body.slug?.current) { revalidateTag(`post-${body.slug.current}`) } } return Response.json({ revalidated: true }) } catch (err) { return Response.json({ message: 'Invalid request' }, { status: 400 }) } } Integration with Nuxt 3
Install the @nuxtjs/sanity module and add configuration in nuxt.config.ts.
// nuxt.config.ts export default defineNuxtConfig({ modules: ['@nuxtjs/sanity'], sanity: { projectId: process.env.NUXT_PUBLIC_SANITY_PROJECT_ID, dataset: 'production', apiVersion: process.env.SANITY_API_VERSION, useCdn: process.env.NODE_ENV === 'production', }, }) In a component, use $sanity for fetch queries:
<template> <article v-if="post"> <h1>{{ post.title }}</h1> <SanityContent :blocks="post.body" /> </article> </template> <script setup lang="ts"> const { $sanity } = useNuxtApp() const route = useRoute() const { data: post } = await useAsyncData(`post-${route.params.slug}`, () => $sanity.fetch( `*[_type == "post" && slug.current == $slug][0] { title, body }`, { slug: route.params.slug } ) ) </script> Integration of Sanity with Remix
In Remix, data is loaded in loader functions on the server — the page renders with content already, no hydration delays or client-side waterfall requests. Use @sanity/client directly in the loader: client.fetch(query, { slug: params.slug }), then pass data to the component via useLoaderData.
Remix does not support ISR out of the box. For caching, use HTTP headers like Cache-Control: s-maxage=3600, stale-while-revalidate=86400 in the loader — Cloudflare and Vercel Edge pick them up automatically. A Sanity webhook invalidates the cache via the Cloudflare Purge API when new content is published. Average response time with a properly configured CDN is 60–100 ms.
Content Typing with Sanity TypeGen
The command npx sanity@latest typegen generate creates TypeScript types from the studio schema. GROQ queries become type-safe: the IDE suggests fields, TypeScript catches typos at build time. This saves 1–2 hours per new content type and reduces production bugs.
Framework Comparison: Speed with Sanity Client
| Framework | First Response Time (TTFB) | Ease of Setup | Documentation |
|---|---|---|---|
| Next.js | Low (ISR/SSR) | High | Excellent |
| Nuxt 3 | Medium (SSR/SSG) | High | Good |
| Remix | Low (loader) | Medium | Good |
In practice, Sanity Client is 2–3 times faster than plain fetch requests thanks to CDN and effective caching.
Common Integration Mistakes
-
Hydration mismatch due to data difference between server and client — solved by synchronizing queries via
draftMode. - CORS errors when making requests from the browser — set up CORS origins in the Sanity dashboard.
-
N+1 queries with nested references — use the GROQ operator
->to fetch related documents. - Stale cache with ISR — ensure the webhook secret is correct and the endpoint is accessible.
What's Included in the Work
- Setting up Sanity Client and API version configuration
- Implementing preview mode (draft mode) with tokens
- Webhook for on-demand revalidation
- GROQ queries and content typing (generated types)
- Image optimization via Sanity Image CDN
- Documentation and team training
How the Process Works
- Analysis: discuss content architecture and query needs
- Design: GROQ query schemas, types
- Implementation: client integration, pages, preview
- Testing: verify hydration, revalidation errors
- Deployment: configure environment variables, webhook
Timelines and Pricing
| Package | What's Included | Timeline |
|---|---|---|
| Basic | Next.js + ISR + Webhooks | 1–2 days |
| Extended | +Nuxt or Remix, preview mode | 2–3 days |
| Turnkey | All frameworks + TypeGen + documentation | 3–5 days |
Cost is determined after analysis of your specific project. Estimated time: Next.js App Router — 1–2 days, Nuxt 3 — 1 day, Remix — 1 day, all three frameworks — 3–5 days. Average cost for a single-framework integration is $1,500–$3,000. Order the integration — we will prepare an accurate estimate. Get a consultation: our engineers will help you choose the optimal architecture.







