Integrate Sanity with Croct
Add personalization and AB testing to your Sanity-powered site.
This integration connects your Sanity project to Croct. Sanity keeps managing your static content, while Croct renders dynamic content on top of it, so you can run AB tests and personalize experiences without migrating your CMS or restructuring your schemas.
How it works
Croct sits alongside Sanity rather than in front of it. Your app fetches content from a slot. When an experience targets the visitor, Croct returns the dynamic variant. If the request fails, the component renders your original Sanity content as the fallback. That makes the integration additive: pages keep working exactly as they do today until you launch your first experience.
Your schemas, documents, and editing workflow stay exactly as they are. Croct reads nothing from Sanity and writes nothing back to it, so you can adopt it one component at a time and roll back by removing a single fetchContent call.
Prerequisites
Before you start, make sure you have:
- A Croct account with a workspace and application set up.
- A Sanity project with a schema and at least one document set up.
Set up the integration
If you are starting from scratch, this demo gives you an example of a working project.
From your project, run the CLI. It detects your setup, installs the SDK, and wires the provider and middleware for you.
npx croct@latest initIf you prefer to set things up by hand, choose your SDK and follow the manual instructions.
Create components and slots
A Croct component defines the content structure that mirrors your Sanity schema, and the slot is the placeholder your application fetches content from. Follow this tutorial to create both for each module you want to make dynamic.
The Croct component must mirror your Sanity schema’s structure, using the same attribute names, so that dynamic content and your Sanity content are interchangeable in the same component.
Implement the slots
For each slot, pass your Sanity content as the fallback when fetching it:
import {fetchContent} from '@croct/plug-next/server';
export async function HomeHero() { const sanityHero = await getSanityHero(); const {content} = await fetchContent('home-hero', { fallback: sanityHero, });
return <Hero {...content} />;}Decide where default content lives
Croct and Sanity can both serve the default content visitors see when no experience or AB test targets them. Choosing which side owns that layer is a structural decision.
Default content in Croct
In the recommended approach, we serve both default and dynamic content, while Sanity serves as the fallback. Once a dynamic zone component is connected, your team manages all of its content in Croct rather than in Sanity. That is what the example above does, as you can see in the demo project.

Keeping the default content in Croct makes experiences and AB tests faster to configure: you can copy the content from the slot and use it as a base, editing only the attributes that should change instead of filling out the entire content every time.
Default content in Sanity
In the alternative approach, we serve only the dynamic content, while Sanity serves both the default and the fallback. Once a component is connected, your team keeps managing default content in Sanity and manages only experience and AB test content in Croct. Teams often prefer this so their editorial workflow stays mostly unchanged once Croct comes into play.

To implement it, check where the content came from before rendering it. When the metadata.contentSource value is slot, the request matched no experience or A/B test, so the result contains no dynamic content, and your Sanity content should be rendered instead.
import {fetchContent} from '@croct/plug-next/server';
export async function HomeHero() { const sanityHero = await getSanityHero(); const {content, metadata} = await fetchContent('home-hero', { fallback: sanityHero, });
// Render the Sanity content unless Croct returned dynamic content const hero = metadata?.contentSource === 'slot' ? sanityHero : content;
return <Hero {...hero} />;}Verify the integration
Once the SDK is running, confirm that dynamic content reaches your site:
- Open your application
Load a page that renders the connected component. You should see the default content.
- Launch a personalized experience
Create and publish a personalized experience or an AB test targeting the connected slot.
- Confirm dynamic content
Reload the page as a visitor who matches your audience. You should see the dynamic content instead of the default.