Architecting the Content Layer: Sanity.io Engineering Deep Dive
Why we chose Sanity over Contentful. A technical guide to Structured Content, GROQ Projections, Portable Text, and Image Pipelines.
In the Headless Commerce ecosystem, Shopify is the Product Database (PIM). It excels at managing SKUs, Prices, and Inventory. It is arguably the world’s worst Content Management System (CMS). Shopify’s “Online Store 2.0” JSON templates are rigid. Metafields are essentially untyped strings. To build a truly dynamic, luxury storefront, you need a dedicated Content Lake.
At Maison Code Paris, we evaluated every Headless CMS (Contentful, Strapi, Prismic). We chose Sanity.io. This article explains the technical reasoning, the architecture, and the implementation details that allow us to build “page builders” that don’t break the code.
Why Maison Code Discusses This
At Maison Code Paris, we act as the architectural conscience for our clients. We often inherit “modern” stacks that were built without a foundational understanding of scale. We see simple APIs that take 4 seconds to respond because of N+1 query problems, and “Microservices” that cost $5,000/month in idle cloud fees.
We discuss this topic because it represents a critical pivot point in engineering maturity. Implementing this correctly differentiates a fragile MVP from a resilient, enterprise-grade platform that can handle Black Friday traffic without breaking a sweat.
The Philosophy: Content as Data, not HTML
In WordPress or simple CMSs, the editor writes “Pages”. They have a WYSIWYG editor where they bold text, add images, and essentially write HTML blobs. This is Presentation Coupled. If you want to reuse that content on an iOS App or a WatchOS notification, you are stuck parsing HTML strings.
Sanity treats content as Structured Data.
A “Hero Section” is not a <div>. It is a JSON object:
{
"_type": "hero",
"heading": "Summer Collection",
"cta": { "label": "Shop Now", "link": "/collections/summer" },
"theme": "dark"
}
The Frontend (React) decides how to render this. The Editor just inputs the intent.
The Query Layer: GROQ vs GraphQL
Most Headless CMSs offer a GraphQL API. It is good. Sanity offers GROQ (Graph-Relational Object Queries). It is exceptional. Why? Because GROQ allows Projections (Client-side reshaping on the server).
Scenario: You have an Author document referenced by a Post. In GraphQL, the schema dictates the shape. In GROQ, you can reshape it on the fly:
// Get all posts, but rename 'author.name' to 'writtenBy'
*[_type == "post"] {
title,
"writtenBy": author->name,
"estimatedReadingTime": round(length(body) / 5 / 60) + " min"
}
Notice the math (round). Notice the pointer dereferencing (->).
We can calculate “Reading Time” on the database layer. We can join datasets. We can projection huge objects into thin DTOs (Data Transfer Objects) perfectly tailored for our React components.
This reduces the payload size by 40-70% compared to standard GraphQL queries that over-fetch.
Architecture: The Content Lake
Sanity is a Real-time Document Store (hosted on Google Cloud). When “Alex” types a character in the Studio, it is synced via WebSocket to the datastore in milliseconds. “Chloe” sees the cursor move instantly (Google Docs style).
This resolves the biggest friction in Headless: Preview. Traditional Headless requires a “Build” (Next.js SSG) to see changes. That takes 2 minutes. Editors hate it. With Sanity + Hydrogen (Remix), we subscribe to the content stream. The Preview is instant.
The React Hook
import { useQuery } from '@sanity/react-loader';
export function useSanityQuery(query, params) {
// If inside the Iframe, use live data. Else use built data.
const { data } = useQuery(query, params);
return data;
}
Portable Text: Solving the “dangerouslySetInnerHTML” Problem
If you let editors write HTML, they will break your site. They will paste a script tag. They will use 5 h1 tags (destroying SEO).
Sanity uses Portable Text. It is a JSON-based specification for rich text.
[
{
"_type": "block",
"style": "normal",
"children": [
{ "text": "Hello " },
{ "text": "World", "marks": ["strong"] }
]
}
]
We render this with a Serializer Component. This gives us total control.
import { PortableText } from '@portabletext/react';
const components = {
// Override how H1 renders
block: {
h1: ({children}) => <h1 className="text-4xl font-bold tracking-tight">{children}</h1>,
},
// Custom Embeds
types: {
instagramPost: ({value}) => <InstagramEmbed id={value.url} />,
productCard: ({value}) => <ProductCard id={value.productId} />
}
};
export const RichText = ({ content }) => (
<PortableText value={content} components={components} />
);
Security: No XSS (Cross Site Scripting) is possible. The structure is strictly typed.
The Image Pipeline
Images are the heaviest part of any e-commerce site. Sanity has a built-in Image CDN. When an editor uploads a 50MB TIFF file, Sanity stores it. The API allows us to request it transformed on the fly.
https://cdn.sanity.io/images/.../my-image.jpg?w=800&h=600&fit=crop&auto=format
We build a reusable Image component that leverages this:
- Hotspot & Crop: The editor sets the “Focus Point” on the model’s face. If we crop to a square, the face is centered automatically.
- Auto Format: Serves AVIF to Chrome, WebP to Safari, JPEG to legacy.
- LQIP (Low Quality Image Placeholder): The metadata includes a base64 encoded blur string. We show this instantly while the main image loads.
// urlBuilder.ts
import imageUrlBuilder from '@sanity/image-url';
const builder = imageUrlBuilder(client);
export function urlFor(source) {
return builder.image(source).auto('format').fit('max');
}
Schema Engineering: Validations
We treat Content Models like Database Schemas. We enforce rules. “A Product Review must have a rating between 1 and 5.”
// schemas/review.ts
defineField({
name: 'rating',
type: 'number',
validation: (Rule) => Rule.required().min(1).max(5).error("Rating must be 1-5")
})
This validation runs in the Studio. The editor physically cannot publish bad data. The frontend code never has to handle rating: 200.
Internationalization (i18n)
Luxury brands are global. There are two strategies in Sanity:
- Field Level:
{ title: { en: "Hello", fr: "Bonjour" } }- Pro: Keeps everything in one document. Good for strong consistent layouts.
- Con: Document gets huge.
- Document Level:
Title_ENdocument andTitle_FRdocument.- Pro: Total freedom per market. The French page can have different sections than the US page.
- Con: Harder to manage sync.
We typically recommend Field Level for “Global Content” (Product Descriptions) and Document Level for “Marketing Pages” (Campaigns often differ by region).
10. Sanity Connect for Shopify (Syncing)
You don’t want to copy-paste Product Titles from Shopify to Sanity. We use Sanity Connect. It listens to Shopify Webhooks. When price updates in Shopify -> Syncs to Sanity. But we make it Read Only in Sanity. The Editor sees the product data, can reference it in a Lookbook, but cannot change the price. This maintains the “Single Source of Truth” (Shopify) while enriching the “Presentation Layer” (Sanity).
11. Custom Studio Dashboards
The CMS is the home meant for the Marketing Team. We build custom Dashboard Widgets in the Studio.
- Google Analytics Widget: “Top 5 Blog Posts this week”.
- Shopify Widget: “Live Sales Ticker”.
- Vercel Build Status: “Is the site deploying?”. This turns the CMS into a Command Center, reducing the need to log into 5 different tools.
13. Migration Strategies: WordPress to Sanity
Migrating 5,000 blog posts from WordPress is scary. We don’t use “Import Plugins”. We write scripts.
- Extract: Connect to WP REST API. Pull all posts.
- Transform: Convert HTML Body -> Portable Text (using
@portabletext/to-portabletext).- This converts
<b>tags tomarks. - It downloads images, uploads them to Sanity Asset Pipeline, and replaces the
<img>src with the new Asset Reference.
- This converts
- Load: Transactional write (100 documents per transaction). Result: A clean, structured dataset from a messy HTML soup.
14. The “Content Lake” Concept
Why call it a “Lake”?
Because you dump everything in it.
Products. Staff Profiles. Store Locations. Legal Policies.
In a traditional CMS, these are siloed.
In Sanity, they are just Documents.
You can link a Store Location to a Staff Profile to a Product.
“This product is available at the Paris store, managed by Chloe.”
This Graph capability allows for incredibly rich frontend experiences that WordPress cannot model.
15. Conclusion
Sanity allows us to decouple the “What” (Content) from the “How” (Presentation). It turns the CMS from a bottleneck into an API. For a developer, querying content with GROQ feels like superpowers. For an editor, seeing their changes live without hitting “Refresh” feels like magic.
Unhappy with your CMS?
If your marketing team breaks the layout every time they publish a blog post.