I Designed My Schema Wrong for Months. Here's What I Learned About Sanity (the Hard Way)

Programming· 4 min read

I Designed My Schema Wrong for Months. Here’s What I Learned About Sanity (the Hard Way)

January 2026. I’d spent weeks on a Sanity content project that was starting to crack at the seams.

The editorial team kept messaging things like: “what does this field mean?” or “is this for mobile or desktop?”. And I had to explain it every time.

The problem wasn’t Sanity. The problem was me, modeling the schema like a traditional relational database form.

Here’s what I learned, straight to the point.

The Mistake Everyone Makes: Booleans Where They Shouldn’t Be

Here’s the typical field you see in almost every project:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Seems reasonable, right? Published or not published.

The problem is that content isn’t binary in real life. You have drafts, revisions, published, archived, scheduled. And when you add a second boolean isFeatured, and a third isArchived, you’ve got a combination of inconsistent states that nobody on the team understands.

The right pattern is string literals:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Now the state is explicit, extensible, and self-documenting. When you need to add scheduled three months from now, you add it to the array. No weird migrations, no ambiguity.

Field Groups: The Schema the Editorial Team Actually Understands

When a document has twenty fields, editors get lost. Field Groups cluster fields by context, not by technical type.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

The writer sees “Content” first. The SEO manager goes to their tab. The developer configures in “Settings”. Everyone in their context. The “I can’t find the field” messages practically disappear.

Custom Previews: The Feature Nobody Configures (That Saves Hours)

By default, Studio shows the first text field as the document title in the list view. With complex documents, that gives you useless lists full of “undefined” or “No title”.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Now the document list in Studio shows the status with an emoji, the title, and the cover image. The editorial team knows at a glance what’s pending review without opening each document.

Validation Rules: Protecting the Team From Themselves

This is genuinely great. Sanity allows synchronous and asynchronous validations directly in the schema.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

The difference between .error() and .warning() is key: errors block publishing, warnings inform without blocking. You guide without frustrating.

For more complex fields, async validations even allow external calls — though I reserve those for specific cases where the request cost is justified.

GROQ and Schema: The Connection People Ignore

What you define in the schema determines how you write your GROQ queries. When you use string literals instead of multiple booleans, your queries get cleaner:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

coalesce() is one of the GROQ functions I use most: returns the first non-null value. Here it uses the manual excerpt if it exists; otherwise, it automatically extracts the first characters of the body.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

The score() function (or _score operator with match) is Sanity’s native way to do relevance search. No external services, no extra configuration.

The Image Pipeline: What Eliminated a Significant Monthly Bill

Before properly understanding Sanity, I was using an external image optimization service. The problem was I was paying for transformations that Sanity’s native pipeline already includes.

Sanity stores hotspot and crop metadata directly in the asset:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

With @sanity/image-url you build optimized URLs on the fly:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

The lqip (Low Quality Image Placeholder) field comes included in the asset metadata. Native blur placeholder with zero extra configuration.

Real-Time Previews: The Bridge That Ends the Tug-of-War With the Editorial Team

This solved a repetitive cycle: the writer would publish, see how it looked in production, request changes, I’d make them, repeat.

With useLiveQuery from next-sanity and Next.js Draft Mode, the Studio shows changes in real time on the site before publishing:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

The writer sees changes as they type. No deploys, no screenshots, no Slack with attachments. Game-changer.

What Changes When You Design the Schema Right

Sanity has a generous free tier (20 seats, 500K API requests per month) that covers most small and medium projects. But the real value isn’t in the pricing — it’s in the time you save when the schema is well-designed from the start.

Schema decisions are the cheapest decisions you make in a content project. Changing them later is expensive. Changing them when you have thirty thousand indexed documents is very expensive.

Here are the concrete steps to apply this week:

  1. Audit your booleans. If you have more than two status booleans on a document, consolidate them into a string literal with explicit options.
  2. Enable Field Groups on any document with more than eight fields. Group by audience (editorial, SEO, technical), not by data type.
  3. Add custom preview to all your main types. Include status with a visual indicator.
  4. Validate SEO fields with character ranges. Block publishing if meta description is empty.
  5. Enable hotspot on all your images and verify you’re using lqip for blur placeholders.

Keep building.

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn