DEV Community

Cover image for Restructure a Sanity schema on a live site without breaking pages
Nayan Kyada
Nayan Kyada

Posted on Originally published at nayankyada.com

Restructure a Sanity schema on a live site without breaking pages

If you need to restructure a Sanity schema on a site that is already live, the fear is reasonable: one renamed field and a page renders blank, or an editor opens a document and half the content has vanished. The good news is that Sanity stores content separately from your schema, so you can change the model safely if you move in the right order. This post is the order I use on inherited and aging projects.

Symptoms of a content model that needs restructuring

You rarely hear "our schema is bad". You hear things like these:

  • Editors paste the same block of copy into five documents because there is no shared type.
  • Pages have fields named heroTitle2, heroTitleNew and heroTitleOld, and nobody knows which one renders.
  • A simple page query is forty lines of GROQ with nested conditionals, because three generations of structure all still exist in the dataset.
  • Developers are afraid to touch the Studio config, so new requests get bolted on instead of modelled.
  • Everything is a single page type with thirty optional fields, so editors see a wall of inputs that mostly do not apply.

These are not cosmetic. They slow every future change, they make queries heavier than they should be, and they are a large part of why editors end up avoiding the Studio. I covered that side in why editors avoid Sanity Studio and the fix.

What it costs to leave it alone

The cost is not a single bill. It is a tax on every task. A new landing page takes three days instead of one because the developer has to work out which fields are live. Bugs appear when someone fills in the legacy field and the frontend reads the new one. Query payloads grow, which shows up in page speed and in your API usage. And the person who understands the mess becomes a single point of failure. If that person is already gone, start with taking over an existing Next.js project.

Step 1: audit what is actually used

Before changing anything, find out what the dataset contains, not what the schema claims. Export a copy with npx sanity@latest dataset export and look at which document types and fields are populated. Count documents per type and check which fields are empty across nearly all documents. Fields that are never filled in can usually be deleted outright, with no migration.

Then do the reverse check: search the frontend for every field a query reads. Anything in the schema that no query touches is a candidate for removal. My process for finding dead types is in auditing and pruning unused Sanity document types.

Write the target model on paper first. Decide which types become reusable objects, which become referenced documents, and which fields collapse into one.

Step 2: work somewhere that is not production

Do not rehearse a restructure on the dataset editors are using. Export production into a separate dataset or a staging project, point a preview deployment at it, and run everything there first. If you only have one dataset today, splitting staging and production safely is the prerequisite step.

Step 3: make the frontend read both shapes

This is the step that prevents downtime. Before any data moves, update your GROQ to accept old and new fields. Deploy that first. Then the data can change underneath without the site noticing.

// lib/sanity/queries.ts - transition query, safe before and after migration
*[_type == "page" && slug.current == $slug][0]{
  _id,
  title,
  "hero": {
    "title": coalesce(hero.title, heroTitle),
    "subtitle": coalesce(hero.subtitle, heroSubtitle)
  }
}
Enter fullscreen mode Exit fullscreen mode

coalesce returns the first value that is not null, so the page renders correctly whether a document has the old flat fields, the new hero object, or both. For more patterns like this, see advanced GROQ queries with joins and coalesce.

Step 4: add the new fields, keep the old ones temporarily

In the Studio, add the new structure alongside the old. Mark the old fields as hidden: true or readOnly: true so editors stop writing to them, but do not delete them yet. If you delete a field from the schema while documents still hold values, Sanity shows those values as unknown fields in the editor, and your migration script becomes harder to reason about.

Step 5: migrate the data with a script

Sanity has a migration tool in the CLI. You create a migration with npx sanity@latest migration create, write the transform, and run it with npx sanity@latest migration run. It runs as a dry run by default, and you pass --no-dry-run to commit changes. Always read the dry-run output first.

// migrations/hero-fields-to-object/index.ts
import {defineMigration, at, set, unset} from 'sanity/migrate'

export default defineMigration({
  title: 'Move flat hero fields into a hero object',
  documentTypes: ['page'],
  migrate: {
    document(doc) {
      if (!doc.heroTitle) return
      return [
        at('hero', set({title: doc.heroTitle, subtitle: doc.heroSubtitle})),
        at('heroTitle', unset()),
        at('heroSubtitle', unset()),
      ]
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Two habits matter here. First, make the script idempotent: the early return means running it twice does nothing the second time. Second, run it against the staging dataset, open a sample of migrated documents in the Studio, and load the preview pages before touching production.

Note that drafts and published versions are separate documents. Check that your migration handles both, otherwise an editor opens a draft next week and finds the old structure back.

Step 6: clean up

Once production is migrated and the site has run cleanly for a while, remove the old fields from the schema and simplify the query back to the new fields only. Regenerate types if you use TypeGen so the compiler flags anything still reading a removed field. That is the point where the dual-read GROQ goes away and the payload shrinks.

What to ask a developer before they start

  • Will you run the migration on a copy of the dataset first, and can I see the dry-run output?
  • How will the live site keep rendering while data moves?
  • What is the rollback plan? A dataset export taken immediately before the run is the minimum answer.
  • How will you handle draft documents and references that point at changed types?
  • Who writes the editor-facing explanation of what changed in the Studio?

A developer who answers these without hesitation has done this before. One who proposes editing production schema and "seeing what happens" has not.

When to get help

A restructure on a small site is a few days of careful work. On a dataset with thousands of documents, multiple locales and several frontends reading it, it deserves a proper plan. This is work I do: I usually begin with the performance and Sanity audit from $800, which produces the target model and a migration plan, before any data is touched.

Top comments (0)