Using Sanity x v0 To Make Your Content Work Harder

January 18, 2026

A little over a week ago Sanity’s MCP server became available in v0. Sanity is a content operating system that provides the infrastructure for managing structured content across your application. When combined with MCP, v0 can leverage that structured content to power real features, not just pages.

What if content weren’t just something users looked at, but something that actively drove behavior? Could structured data do more than display text on a screen? While modern applications tend to be content-heavy, they simply end at the interface. With structured content, we can unlock features that understand the relationship of our content.

In this article, we’ll explore that idea and dive into my own example to see how apps can leverage structured content to power features users actually care about.

When Content Becomes Decision Making

In most modern applications, content is treated as passive data. It gets fetched, transformed, and wired together with application logic that decides how the system behaves. As systems become more composable, this model begins to strain. Simple changes carry more risk, since a new content rule often requires touching multiple layers or redeploying services.

With Sanity, content can take on more responsibility, driving more than just visualization. Consider a product finder that guides parents through age, dietary needs, and product preferences, then surfaces relevant recommendations. Those recommendations are powered by content relationships, offering a glimpse of what content infrastructure can look like when we stop thinking in terms of pages.

Why This Pattern Matters In Real Systems

This shift is not just architectural. It has practical implications for how teams ship, who owns behavior, and how systems scale. A helpful analogy can be found in how structured context is used in agent systems. Projects like https://skills.sh/ provide curated markdown files that teams can load into their projects, giving agents consistent, structured context to reason over. The value comes from the structure and constraints of the content, not from the mechanism that consumes it.

Sanity applies a similar idea to content infrastructure. Instead of treating content as something to render, it treats it as structured context that downstream systems can use to make decisions.

Turning Structured Content Into Functionality

I put these ideas to test when building Typeshift. The Valentine app is intentionally playful, but it serves as a concrete example of this pattern in action. Content serves as the foundation for the entire experience. Profiles, preferences, and matches are modeled as queryable documents in Sanity, allowing the frontend to derive real functionality directly from content rather than hardcoded logic.

Instead of treating profiles as static representations, each profile includes structured fields such as interests, availability, intent, and lifestyle tags. This data powers advanced filtering, match scoring, and 1:1 matchmaking decisions. Matches themselves are also stored as structured content, which makes it possible to track confirmed pairs and surface them in features like a success stories view as the system evolves.

By relying on queries over structured content, the app avoids a traditional backend while still supporting meaningful application behavior. Updating content in Sanity directly affects how the app behaves, demonstrating how a content platform can act as application infrastructure rather than just a publishing layer.Features Powered by Structured Content

The system is built around structured menu and order data, which allows features to emerge naturally from queries rather than hardcoded logic.

Dynamic Matchmaking Logic

For demo purposes, the user journey begins with a prompt for user data that is used to build their profile. Vercel’s AI-SDK leverages this data to create a 1:1 match for the user and returns a match powered by Sanity’s structured content.

Advanced Filtering and Sorting

Structured content unlocks filter possibilities like no other. Modern dating apps end at interests where Typeshift can dive into specifics. Interests overlap, so I used our schemas to filter down to a user’s favorite artists, movies, travel destinations, and even specific availability.

Match Explanation & Transparency

The User Stories page is schema driven and serves as context for the AI-SDK to tell the love story of our users. Because matches are based on schema fields, you can annotate the logic: which fields overlapped, what weights were met, etc.

Implementation Details

Modeling Profiles

The Profile document allows each profile to be modeled as a collections of intentional, queryable fields designed to support behavior.

Age range, dating intent, and languages are all constrained to predefined values. This ensures consistency in the data powering meaningful filtering, such as finding provides with similar music tastes or shared languages.

sanity/schemas/profile.tstypescript
import { defineType, defineField } from "sanity";

export const profile = defineType({
  name: "profile",
  title: "Profile",
  type: "document",
  fields: [
    defineField({
      name: "name",
      title: "Name",
      type: "string",
      validation: (Rule) => Rule.required().min(2).max(100),
    }),
    defineField({
      name: "ageRange",
      title: "Age Range",
      type: "string",
      options: {
        list: [
          { title: "18-24", value: "18-24" },
          { title: "25-34", value: "25-34" },
          { title: "35-44", value: "35-44" },
          { title: "45-54", value: "45-54" },
          { title: "55-64", value: "55-64" },
          { title: "65+", value: "65+" },
        ],
        layout: "dropdown",
      },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "race",
      title: "Race/Ethnicity",
      type: "array",
      of: [{ type: "string" }],
      options: {
        list: [
          { title: "Asian", value: "asian" },
          { title: "Black or African American", value: "black" },
          { title: "Hispanic or Latino", value: "hispanic" },
          { title: "Middle Eastern or North African", value: "mena" },
          { title: "Native American or Alaska Native", value: "native-american" },
          { title: "Native Hawaiian or Pacific Islander", value: "pacific-islander" },
          { title: "White", value: "white" },
          { title: "Multiracial", value: "multiracial" },
          { title: "Other", value: "other" },
          { title: "Prefer not to say", value: "prefer-not-to-say" },
        ],
        layout: "grid",
      },
    }),
    defineField({
      name: "ethnicity",
      title: "Additional Ethnicity",
      type: "string",
      options: {
        list: [
          { title: "East Asian", value: "east-asian" },
          { title: "South Asian", value: "south-asian" },
          { title: "Southeast Asian", value: "southeast-asian" },
          { title: "West Asian/Middle Eastern", value: "west-asian" },
          { title: "North African", value: "north-african" },
          { title: "Sub-Saharan African", value: "sub-saharan" },
          { title: "Caribbean", value: "caribbean" },
          { title: "Latin American", value: "latin-american" },
          { title: "European", value: "european" },
          { title: "Indigenous/Native", value: "indigenous" },
          { title: "Prefer not to say", value: "prefer-not-to-say" },
        ],
        layout: "dropdown",
      },
    }),
    defineField({
      name: "languages",
      title: "Languages Spoken",
      type: "array",
      of: [{ type: "string" }],
      options: {
        list: [
          { title: "English", value: "english" },
          { title: "Spanish", value: "spanish" },
          { title: "French", value: "french" },
          { title: "Mandarin Chinese", value: "mandarin" },
          { title: "Cantonese", value: "cantonese" },
          { title: "Japanese", value: "japanese" },
          { title: "Korean", value: "korean" },
          { title: "Vietnamese", value: "vietnamese" },
          { title: "Tagalog", value: "tagalog" },
          { title: "German", value: "german" },
          { title: "Italian", value: "italian" },
          { title: "Portuguese", value: "portuguese" },
          { title: "Arabic", value: "arabic" },
          { title: "Hebrew", value: "hebrew" },
          { title: "Hindi", value: "hindi" },
          { title: "Bengali", value: "bengali" },
          { title: "Thai", value: "thai" },
          { title: "Russian", value: "russian" },
          { title: "Polish", value: "polish" },
          { title: "Greek", value: "greek" },
        ],
        layout: "grid",
      },
    }),
    defineField({
      name: "intent",
      title: "Dating Intent",
      type: "string",
      options: {
        list: [
          { title: "Casual Dating", value: "casual" },
          { title: "Serious Relationship", value: "serious" },
          { title: "Marriage", value: "marriage" },
          { title: "Friendship First", value: "friendship" },
          { title: "Open to Anything", value: "open" },
        ],
        layout: "radio",
      },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "lifestyleTags",
      title: "Lifestyle Tags",
      type: "array",
      of: [{ type: "string" }],
      options: {
        list: [
          { title: "Active & Athletic", value: "active" },
          { title: "Homebody", value: "homebody" },
          { title: "Adventurer", value: "adventurer" },
          { title: "Creative", value: "creative" },
          { title: "Career-Focused", value: "career" },
          { title: "Family-Oriented", value: "family" },
          { title: "Social Butterfly", value: "social" },
          { title: "Introvert", value: "introvert" },
          { title: "Pet Lover", value: "pets" },
          { title: "Foodie", value: "foodie" },
        ],
        layout: "grid",
      },
    }),
    defineField({
      name: "bio",
      title: "Bio",
      type: "text",
      rows: 4,
      validation: (Rule) => Rule.max(500),
    }),
    defineField({
      name: "profileImage",
      title: "Profile Image",
      type: "image",
      description: "AI-generated portrait image",
      options: {
        hotspot: true,
      },
      fields: [
        defineField({
          name: "alt",
          title: "Alt Text",
          type: "string",
        }),
        defineField({
          name: "caption",
          title: "Caption",
          type: "string",
        }),
      ],
    }),
    defineField({
      name: "interests",
      title: "Interests",
      type: "array",
      of: [
        {
          type: "reference",
          to: [{ type: "interest" }],
        },
      ],
      validation: (Rule) => Rule.unique(),
    }),
    defineField({
      name: "availability",
      title: "Availability",
      type: "array",
      of: [
        {
          type: "reference",
          to: [{ type: "availability" }],
        },
      ],
      validation: (Rule) => Rule.unique(),
    }),
    defineField({
      name: "itsAMatch",
      title: "Its a Match",
      type: "boolean",
      description: "Demo flag to indicate a match",
      initialValue: false,
    }),
    defineField({
      name: "createdAt",
      title: "Created At",
      type: "datetime",
      initialValue: () => new Date().toISOString(),
      readOnly: true,
    }),
  ],
  preview: {
    select: {
      title: "name",
      subtitle: "intent",
      media: "profileImage",
    },
  },
  orderings: [
    {
      title: "Created (Newest)",
      name: "createdAtDesc",
      by: [{ field: "createdAt", direction: "desc" }],
    },
    {
      title: "Name A-Z",
      name: "nameAsc",
      by: [{ field: "name", direction: "asc" }],
    },
  ],
});

Modeling Interests & Availability

Interests and availability are intentionally separated rather than stored as simple arrays of strings. By modeling interests and availability as first-class documents, they gain identity, relationships, and the ability to evolve independently of profiles.

sanity/schemas/interests.tstypescript
import { defineType, defineField } from "sanity";

export const interest = defineType({
  name: "interest",
  title: "Interest",
  type: "document",
  fields: [
    defineField({
      name: "name",
      title: "Interest Name",
      type: "string",
      validation: (Rule) => Rule.required().max(50),
    }),
    defineField({
      name: "category",
      title: "Category",
      type: "string",
      options: {
        list: [
          { title: "Sports & Fitness", value: "sports" },
          { title: "Arts & Culture", value: "arts" },
          { title: "Music", value: "music" },
          { title: "Food & Dining", value: "food" },
          { title: "Travel", value: "travel" },
          { title: "Technology", value: "technology" },
          { title: "Outdoors", value: "outdoors" },
          { title: "Entertainment", value: "entertainment" },
          { title: "Wellness", value: "wellness" },
          { title: "Other", value: "other" },
        ],
        layout: "dropdown",
      },
    }),
    defineField({
      name: "slug",
      title: "Slug",
      type: "slug",
      options: {
        source: "name",
        maxLength: 50,
      },
    }),
  ],
  preview: {
    select: {
      title: "name",
      subtitle: "category",
    },
  },
});

Querying

By treating querying as application logic, TypeShift demonstrates how structured content can replace entire layers of traditional backend complexity while remaining transparent and easy to reason about.

Instead of asking whether two bios contain similar words, TypeShift can ask whether two profiles reference the same interest documents or compatible availability slots. This makes matches explainable and deterministic.

Matches

Matches include a sharedInterests array and a compatibilityScore. These fields are computed from structured profile data, not freeform text. By storing them on the match itself, queries can sort or filter matches by compatibility or mutual interests.

Other Real World Use Cases

This pattern of treating content as structured context that drives system behavior is not limitied to CMS projects. Many successful products and platforms apply it to create new capabilities and experiences.

Take Spotify Wrapped as an example. Early user feedback showed that personalized summaries of listening habits could drive engagement and excitement. Wrapped leverages content (user data) structured in way that powers new experiences, inspiring countless other platforms to adopt similar “wrapped” features.

  • Personalization rules: content defines what users see based on behavior or preferences
  • Feature gating: structured content determines which features are available to whom
  • AI prompt orchestration: content feeds and constrains AI prompts dynamically

These examples show the same principle: content as structured, decision-driving context can be applied across domains, making systems more adaptable, responsive, and user-focused.

When This Pattern Isn’t Always the Right Fit

While treating content as structured, decision-driving context can simplify systems and accelerate iteration, it’s not a one-size-fits-all solution. Like any architectural pattern, it comes with tradeoffs teams should consider.

  • Performance limits - Complex decision logic in content layers can increase query times or reduce caching efficiency. For high-throughput scenarios, some decisions are still best handled in the backend.
  • Ownership boundaries - Not all stakeholders should control every decision. Editorial teams can influence behavior safely, but certain rules still require engineering oversight.

Systems with tight transactional constraints, complex orchestration, or sensitive data may benefit more from backend logic. Understanding these tradeoffs helps teams apply the pattern deliberately, knowing where content-driven decision-making adds value and where conventional backend approaches remain more appropriate.

Lessons Learned

Working on this project reinforced just how powerful structured content can be when used a decision-driving layer, not just for rendering interfaces. Treating fields, relationships, and constraints as first-class elements allows systems to make intelligent, predictable choices based on the content itself.

Some key takeaways:

  • Structured content drives behavior: Carefully modeled content unlocks precise outcomes, whether that’s personalization, recommendations, or feature gating.
  • Query-driven systems are flexible: Content-aware queries enable dynamic behavior without touching the underlying code, accelerating iteration.
  • Design for context, not just display: Providing the right structure upfront allows downstream systems (including AI workflows) to act with confidence, reducing friction.
  • Patterns scale beyond prototypes: Even small systems demonstrate that thoughtful content modeling creates opportunities for composable, adaptable architectures.

Structured content is increasingly the foundation for composable systems and AI-enhanced workflows. By treating content as configuration rather than static data, teams can:

  • Ship faster and iterate safely
  • Empower stakeholders to influence system behavior
  • Integrate AI and edge computing without rewriting core logic

This approach shows how thoughtful content infrastructure can be a competitive advantage, supporting richer experiences, more flexible architectures, and systems that scale gracefully as needs evolve.

If you’re interested in experimenting with Sanity and v0, check out v0 and Sanity docs, or explore the v0 Chat for Typeshift.

Liked this article? Share it with a friend on Bluesky or X. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.

Portfolio⮡2025⚗✨

Emmanuel Perez

☼Developer & Writer☀

Based New Jersey

Copyright 2025 © Emmanuel Perez