Who Owns the Content Model

I am a Solutions Engineer at Optimizely. egandalf.com runs on Sanity. I picked Sanity for this site because it's one developer, no marketing team, no approval chain, and a content model I wanted to keep in a repo next to the code that renders it. Those conditions describe almost none of the enterprise deals I work on, which is the whole point of what follows.
People ask which platform is better. That question has no answer. A more useful question is who in your organization is allowed to change the content model and how long that change takes. Nearly every meaningful difference between these two products falls out of how each one answers it.
Which Optimizely, and why it matters less than you would think
Optimizely's position is that CMS 12, 13, and SaaS are one CMS hosted and delivered differently, and for this comparison that holds up. The content model concepts are shared across all of them: base types of page, block, and component, a common property system, and Optimizely Graph as the delivery layer. CMS 13 made Graph and Opti ID required components rather than optional add-ons, which pulled the self-managed line toward the SaaS one rather than away from it.
What differs is delivery. Self-managed keeps the option of server-side ASP.NET MVC rendering against the same content. SaaS is fully managed and headless-first: content in, API out, front end yours.
I am comparing Sanity to the SaaS delivery, because that's the matchup where both products are doing the same job. Most of what follows applies either way, since the content model behaves the same regardless of where the CMS runs. Where the delivery model changes the answer, I say so.
Where the model lives
In Sanity, the content model is TypeScript in your repository.
export const personType = defineType({
name: 'person',
type: 'document',
fields: [
defineField({ name: 'name', type: 'string' }),
defineField({ name: 'portrait', type: 'image' }),
],
})That object is the schema. It generates the editing form, it generates TypeScript types through TypeGen, and it lives in version control with everything else. Adding a field is a commit, which puts the model change inside your existing SDLC: same branch, same review, same release process as the rest of the application. What that does not get you is any change to existing content. The Content Lake underneath is schema-less so the schema is a contract enforced at the edges rather than a database migration. Documents written before the change keep their old shape until something rewrites them and deploying the schema is its own step after the merge.
Optimizely gives you both options and this is where the delivery model actually changes the answer.
In self-managed delivery, content types are C# classes in your solution, decorated with attributes and compiled into the application. That's code-first, it lives in source control, and it goes through the same review and release process as the rest of the codebase. Structurally it's much closer to the Sanity approach than most comparisons admit.
In SaaS delivery, content types are a resource you manage over the wire. You POST JSON to /v1/contenttypes with a key, a baseType of _page, _block, or _component, and a properties object, or an administrator defines the same thing through the UI. Both paths write to the same place. You can script the API calls and keep those definitions in a repo, and teams that care about this do exactly that but the repo is then a source you push from rather than the system of record.
I have shipped content models both ways. My first CMS work was Ektron, where content types were Smart Forms, configured XML rather than compiled types. Then EPiServer, which became Episerver, which everyone called Epi, which is now Optimizely. I came in at CMS 7, where the content model was C# classes in the solution and had been since before I arrived. Writing Sanity schemas in TypeScript felt like a language change, not a new idea. Schema-as-code gets sold as a headless innovation. It has been the default in .NET CMS for more than a decade and the configured model I started on never went away. It moved to SaaS.
So the real question is not which product. It is whether the content model is compiled into an artifact you deploy or managed as live configuration.
Compiled means reviewable, revertible, testable, and it means a developer and a release for every field. Live configuration means a trained administrator can add a property on a Tuesday afternoon without touching a build pipeline and it means the change history lives in an audit log rather than a git log.
If your content model changes twice a year and your marketing team is large, the second is probably fine. If it changes twice a week and your team is four engineers, you want the first. I have watched organizations pick the wrong one in both directions and then blame the software. It's worth noting that Optimizely lets you make that choice and Sanity does not.
Where the business logic lives
The content model question has a second half that comparisons usually skip and it's the one that separates PaaS from everything else.
In a self-managed Optimizely deployment you have the application codebase. That means custom validation that runs on the server, content events that can inspect and cancel a publish before it happens, initialization modules, scheduled jobs, dependency injection, custom property editors, and back-end integrations executing in-process with direct access to the content repository. Business logic sits in the write path. If a rule says a product page can't publish without a valid SKU that resolves against an ERP, that rule can be enforced at the moment of publish, by code you own, inside the application.
Neither SaaS CMS nor Sanity gives you that, though for different reasons.
Optimizely SaaS has no application of yours to run code in. The logic moves outward, to your front end or to services you reach through the APIs.
Sanity does give you an application. Studio is yours: you build it, you own it, you deploy it. But it's still a client, not a server. It runs in the editor's browser and the Content Lake accepts writes that never pass through it, whether from the API, a client library, a migration script, or an agent. Studio-side you get schema validation rules, document actions, and custom input components in React, and every one of them governs the person typing rather than the data. Server-side you get Functions, defined and deployed through Blueprints, which run on document events or a schedule, plus GROQ-powered webhooks that fire when content changes. Those run after a change lands, not before it.
By Sanity's own documentation, schema validation rules only run in Sanity Studio. Mutations submitted through the API or client libraries are not checked against them. A required field is required for a human typing in the Studio and optional for a migration script, an integration, or an agent writing through the client. You can validate a whole dataset in bulk from the CLI after the fact and you can build the check into whatever is doing the writing. The content store itself will still accept the document.
Optimizely doesn't have this gap in either delivery mode. Content modeling sits at the data layer rather than in the application or the editing UI, which is why hosting doesn't change the answer. A typed property won't take the wrong type and a required property is required whether the write came from an editor, the Content Management API, or an integration. You also can't push a property that was never defined. The type is closed so an integration that invents a field gets rejected rather than stored.
The Content Lake does the opposite. It takes the field, the document ends up carrying something the schema has never heard of, and Studio has no input to show it in. That's what a typed schema buys you: the guarantee holds at the boundary, not at the keyboard.
So the honest framing is interception versus reaction. PaaS can refuse a write. SaaS CMS and Sanity observe one and respond to it.
In-process business logic is also what makes upgrades expensive, couples your rules to a CMS release cycle, and puts third-party integration failures directly inside the publishing path. Anyone who has debugged a publish that hangs because an event handler is waiting on a downstream service knows exactly what that trade costs. Externalized logic scales independently, fails independently, and gets versioned independently. It just cannot hold a line at the moment of write.
Which one you want depends on whether the CMS is the system of record for rules that must never be violated or a content store surrounded by services that own the rules. Both are legitimate architectures. Only one of them survives contact with a compliance requirement that says the invalid document must not exist.
Querying
Optimizely Graph is GraphQL. Content types you define in the CMS map to queryable GraphQL types, every item carries a _metadata object with display name, locale, URL data, and publish timestamps, and the schema regenerates as your types change. You get full-text and fuzzy search, faceting, geolocation, cursor pagination. CMS 13 added a .NET SDK with a fluent typed API for the C# side.
Sanity uses GROQ, which is its own language. Instead of calling a schema-declared root field, you filter the whole document space and project what you want:
*[_type == "post" && title match "hello*"]{
title,
author->{name, bio},
"displayPrice": coalesce(salePrice, regularPrice),
"reviewCount": count(reviews)
}Three things in that query would require server-side resolver code in a GraphQL implementation. The -> traverses a reference without the relation being declared anywhere. The displayPrice is computed inline. The count is an aggregate in a projection. GROQ also does reverse lookups with references(^._id), full-text through text::query(), and vector ranking through text::semanticSimilarity() once dataset embeddings are on.
GraphQL is a specification with a decade of tooling, a hiring pool, and Apollo. GROQ exists in exactly one product. Sanity closes part of that gap with TypeGen, which generates result types from queries the way GraphQL codegen does and it will deploy a GraphQL API over your data if you want one. But if your team already has GraphQL infrastructure and expertise, the switching cost is real and Sanity's own documentation says so.
GROQ is more expressive per line and less portable per engineer.
The editing surface
This is the largest practical difference and the one most likely to decide a deal.
Optimizely's Visual Builder is a finished product. Drag and drop, real-time preview, templates and blueprints for reusable structures, and in CMS 13 a unified interface across pages, blocks, experiences, and media that replaced on-page editing as the default. Content Manager sits alongside it as a search-first navigation layer powered by Graph. An editor sits down and composes a page. Nobody had to build that.
Sanity Studio is an application you build and deploy. Your schema generates the forms and past that, customization is a React programming task. Visual editing isn't a feature you enable so much as an architecture you assemble: the client encodes Content Source Maps into strings as zero-width Unicode characters, the visual-editing package scans the DOM and draws click-to-edit overlays, Comlink carries postMessage traffic between the Studio and your iframe, and the Presentation Tool resolves documents to front-end routes. Framework libraries like next-sanity wrap most of it. You still own the wiring.
The result is that Sanity's editor experience is exactly as good as what you built and Optimizely's is exactly as good as what Optimizely built. For a team that wants a bespoke authoring interface shaped around a specific editorial workflow, the first is the only real option. For a marketing organization of forty people who need to ship campaign pages without filing engineering tickets, the second is not a compromise, it is the requirement.
Sanity implementations fail when the Studio never gets invested in and editors quietly go back to asking developers for changes. Optimizely implementations fail when a team fights the composition model for a year because what they actually wanted was a headless API and a custom admin.
Rich text and reuse
Sanity stores rich text as Portable Text, a structured JSON array of blocks that's renderer-agnostic. You define custom block types in the schema and write a renderer per output target. Nothing about the stored content assumes HTML, which matters when the same paragraph has to render in a web page, a mobile app, and an email.
Optimizely uses an XHTML string property alongside content areas and shared blocks, with CMS 13 automatically inlining shared blocks into Graph.
Portable Text is better for genuine multichannel reuse and worse for everything about migration. HTML is universally understood, every legacy system emits it, and every editor has pasted into it for twenty years. Portable Text requires you to build renderers before anything appears on a screen. It also doesn't degrade into unmaintainable markup the way an HTML field does after five years of pasting from Word.
What you are actually buying
Sanity sells the content layer and grows by composing with best of breed. Media Library, Canvas, Content Lake, Studio. Experimentation, personalization, analytics, and campaign management come from vendors you pick, which is the strategy rather than a hole in it.
Optimizely sells a more complete package. CMS, Experimentation, CMP, DAM, Personalization, Analytics, and Opal on top, tied together by Opti ID for identity and role-based access across products, and by Graph as a shared query layer that indexes external sources alongside CMS content and can power site search. The suite is also not a closed box. Graph indexing external sources is the clearest sign of it and bringing your own tools is a supported path rather than a workaround.
Every honest version of this comparison lands in the same place. The suite reduces integration surface, gives you one contract and one support path, and increases coupling. The layer maximizes best-of-breed choice and swap-ability, and hands you the integration work plus a stack of vendor relationships.
When the writer is an agent
Every question above gets sharper once the thing writing content isn't a person.
Sanity's agent actions operate against your deployed schema so the model works with your actual content structure rather than free text. Canvas maps unstructured drafts into structured documents through that same schema. Semantic similarity is a GROQ function. That's a better foundation than most AI content tooling starts from.
But an agent writing through the client is on the API path, not the Studio path. Everything in the validation section applies to it: required fields aren't required, undefined properties are accepted, and the check has to live in whatever you built around the write.
In Optimizely the agent hits the same content model as everyone else. Opal spans the suite so an agent can reach across content, experimentation, and campaign data in one workflow, and whatever it writes back faces the same typed, closed content type a human editor faces.
Neither is a superset of the other. Sanity's is deeper in the content layer and Optimizely's is broader across the stack. The thing worth settling before you pick either is whether you want an agent's output validated by the system or by code you wrote around it.
Why I picked Sanity
The honest version has less architecture in it than everything above.
The front end existed first. This site is Next.js on Vercel and I wanted a content layer that dropped into it without an argument. Sanity did, in about an evening. The stack picked the CMS, which is the reverse of every enterprise evaluation I have ever run, where the platform gets chosen in a boardroom and the front end is whatever survives contact with it.
Cost and friction did the rest. Free tier, no infrastructure, no license conversation, nothing to provision on a Saturday.
Optimizely was never really in the running here, though not for a product reason. Running your employer's platform for personal work carries complications that have nothing to do with whether it's the right tool. Worth saying plainly rather than letting you assume it lost a bake-off it was never entered in.
Then there's the part I'd have admitted last. I wanted to run it. Reading a competitor's documentation tells you what they claim. Living in their product tells you what they built and there's no substitute for being the person who has to fix it at eleven at night.
I never invested in the Studio. It's close to stock, because I'm the only person who uses it and I'd rather write a schema file than build an editing experience for an audience of one. I'm not a marketer. I'm an old developer and bare bones suits me fine. Put a content team behind this site and the same decision turns into the problem I described.
The thing I didn't expect was how little of it felt new. Smart Forms to content classes to schema files is three serializations of one idea. The twenty years in between changed the language more than the argument. What has actually changed is that the writer might not be a person anymore. That puts the oldest question in this business back on the table: who is allowed to change the content model and what stops them when they get it wrong?
Written and published with help from Claude.


