MAISON CODE .
/ Headless CMS · Sanity · Backend · Architecture · Content Strategy

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.

AB
Alex B.
Architecting the Content Layer: Sanity.io Engineering Deep Dive

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:

  1. Hotspot & Crop: The editor sets the “Focus Point” on the model’s face. If we crop to a square, the face is centered automatically.
  2. Auto Format: Serves AVIF to Chrome, WebP to Safari, JPEG to legacy.
  3. 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:

  1. Field Level: { title: { en: "Hello", fr: "Bonjour" } }
    • Pro: Keeps everything in one document. Good for strong consistent layouts.
    • Con: Document gets huge.
  2. Document Level: Title_EN document and Title_FR document.
    • 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.

  1. Google Analytics Widget: “Top 5 Blog Posts this week”.
  2. Shopify Widget: “Live Sales Ticker”.
  3. 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.

  1. Extract: Connect to WP REST API. Pull all posts.
  2. Transform: Convert HTML Body -> Portable Text (using @portabletext/to-portabletext).
    • This converts <b> tags to marks.
    • It downloads images, uploads them to Sanity Asset Pipeline, and replaces the <img> src with the new Asset Reference.
  3. 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.

Hire our Architects.