✨Discover storytelling in the AI age with Pixar's Matthew Luhn at Sanity Connect, May 8th—register now

Example: Migrating the blog template from Studio v2 to v3

A step-by-step example on how to migrate a Sanity Studio from v2 to v3

Let's say you have installed the Studio v2 of the blog template from the Sanity CLI and customized it with Structure Builder and a custom logo. Now you want to migrate this Studio to v3 in place.

In most cases, going from Studio v2 to v3 will mean less code and fewer dependencies. Because Studio v3 introduces configuration as runnable code, it should also bring you even more flexibility, composability, and customization opportunities.

What you roughly have to do is the following:

  • Remove the Studio v2 dependencies, install and upgrade the v3 dependencies
  • Make a sanity.config.js and sanity.cli.js file in the project's root folder
  • Move the values for projectId and dataset (and optionally project.name) from sanity.json to sanity.config.js
  • Edit schema.js and import the schemas to sanity.config.js
  • Add the structureTool and visionTool plugins to sanity.config.js
  • Edit deskStructure.js and import it into sanity.config.js
  • Replace the settings.js' __experimental_actions with configuration in sanity.config.js
  • Edit the Logo.js file and import it into sanity.config.js
  • Clean up and delete the files you don't need anymore:
    • The config folder
    • All sanity.json files

Update dependencies

Your Studio v2‘s package.json will look something like this:

{
  "name": "blog",
  "private": true,
  "version": "1.0.0",
  "description": "",
  "main": "package.json",
  "author": "Knut Melvær <knut@sanity.io>",
  "license": "UNLICENSED",
  "scripts": {
    "start": "sanity start",
    "build": "sanity build"
  },
  "keywords": [
    "sanity"
  ],
  "dependencies": {
    "@sanity/base": "^2.35.2",
    "@sanity/core": "^2.35.2",
    "@sanity/default-layout": "^2.35.2",
    "@sanity/default-login": "^2.35.2",
    "@sanity/desk-tool": "^2.35.2",
    "@sanity/vision": "^2.35.2",
    "prop-types": "^15.8",
    "react": "^17.0",
    "react-dom": "^17.0",
    "styled-components": "^5.3.6"
  },
  "devDependencies": {}
}

Delete deprecated packages

You can run the following to delete the dependencies you don't need anymore:

yarn remove @sanity/base @sanity/core @sanity/default-layout @sanity/default-login @sanity/desk-tool

# Using npm
npm uninstall @sanity/base @sanity/core @sanity/default-layout @sanity/default-login @sanity/desk-tool

Upgrade to React 18

And then you have to upgrade your React dependencies to 18 or above:

yarn add react@latest react-dom@latest prop-types@latest

# Using npm
npm install react@latest react-dom@latest prop-types@latest

Gotcha

If you have installed @sanity/ui, you'll need to upgrade it to the latest version as well: yarn add @sanity/ui@latest

Install Studio v3 and upgrade plugins

Now you can install sanity and upgrade your Vision plugin for Studio v3:

yarn add sanity@latest @sanity/vision@latest

# Using npm
npm install sanity@latest @sanity/vision@latest

Protip

We have made many of the popular Sanity Studio plugins ready for v3. You can install them with @latest as well.

Add the new sanity dev npm script

Studio v3 introduces a breaking change: sanity start is now used to preview production builds of the studio (sanity build). To run Studio v3 in developer mode, you now have to run sanity dev. You can add this to package.json like this:

{
  // ...rest of the config
  "scripts": {
    "start": "sanity start",
    "build": "sanity build",
"dev": "sanity dev"
}, // ...rest of the config }

And that's it! Now your package.json should look something like this:

{
  "name": "blog",
  "private": true,
  "version": "1.0.0",
  "description": "",
  "main": "package.json",
  "author": "Knut Melvær <knut@sanity.io>",
  "license": "UNLICENSED",
  "scripts": {
    "start": "sanity start",
    "build": "sanity build",
    "dev": "sanity dev"
  },
  "keywords": [
    "sanity"
  ],
  "dependencies": {
    "@sanity/vision": "^3.0.6",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "sanity": "^3.0.6",
    "styled-components": "^5.3.6"
  },
  "devDependencies": {}
}

Add Studio v3 configuration

Studio v3 introduced a new JavaScript-based configuration, hence deprecating the JSON-based configuration in sanity.json.

Now you can create 2 new files in the root folder: sanity.config.js and sanity.cli.js.

sanity.config.js

Copy the following code and paste it into your sanity.config.js file:

// sanity.config.js
import { defineConfig } from "sanity";

export default defineConfig({
  title: "",
  projectId: "",
  dataset: "",
  plugins: [],
  schema: {
    types: [],
  },
});

Open sanity.json and copy the values for project.name, api.projectId, and api.dataset over:

// sanity.config.js
import { defineConfig } from "sanity";

export default defineConfig({
title: "blog",
projectId: "80ji1j7a",
dataset: "production",
plugins: [], schema: { types: [], }, });

Now you should be able to run npm run dev (npx sanity dev) and check if your Studio v3 is running on http://localhost:3333. Note that it will be empty since we haven't imported Structure Tool or any schemas yet.

Add Structure Tool and schemas

Start by importing structureTool and adding it to the plugins array:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
export default defineConfig({ title: "blog", projectId: "80ji1j7a", dataset: "production",
plugins: [structureTool()],
schema: { types: [], }, });

Now you can open schema.js and get rid of everything besides the imports of schema files/variables and the array of schemas.

Before (Studio v2)

import createSchema from 'part:@sanity/base/schema-creator'
import schemaTypes from 'all:part:@sanity/base/schema-type'

import blockContent from './blockContent'
import category from './category'
import post from './post'
import author from './author'
import settings from './settings'

export default createSchema({
  name: 'default',
  types: schemaTypes.concat([
    post,
    author,
    category,
    blockContent,
    settings
  ]),
})

After (Studio v3)

// schemas/schema.js
import blockContent from './blockContent'
import category from './category'
import post from './post'
import author from './author'
import settings from './settings'

export default [
  post,
  author,
  category,
  blockContent,
  settings
]

Now, import the schemas into sanity.config.js:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
import schemas from './schemas/schema'
export default defineConfig({ title: "blog", projectId: "80ji1j7a", dataset: "production", plugins: [structureTool()], schema: {
types: schemas,
}, });

If you save your changes, then your document types should appear in the Studio's root pane.

Desk Structure

This blog has a so-called “singleton” for the settings.js schema to ensure that we only have one settings document in the Studio. It uses the Structure Builder API to achieve this. Start by preparing the deskStructure.js file

Before (Studio v2)

// deskStructure.js
import S from '@sanity/desk-tool/structure-builder'

export default () => S.list().title('Content').items([
  S.listItem()
    .title('Settings')
    .child(
      S.editor()
        .id('settings')
        .schemaType('settings')
        .documentId('settings')
    ),
  ...S.documentTypeListItems().filter(listItem => !['settings'].includes(listItem.getId()))
])

After (Studio v3)

Delete the S import of structure builder, and move the S variable as a function argument

// deskStructure.js
export default (S) => S.list().title('Content').items([
S.listItem() .title('Settings') .child( S.editor() .id('settings') .schemaType('settings') .documentId('settings') ), ...S.documentTypeListItems().filter(listItem => !['settings'].includes(listItem.getId())) ])

Now we can import the custom structure into sanity.config.js and the structureTool configuration like this:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
import schemas from './schemas/schema'
import deskStructure from './deskStructure'
export default defineConfig({ title: "blog", projectId: "80ji1j7a", dataset: "production", plugins: [structureTool({
structure: deskStructure
})], schema: { types: schemas, }, });

The Logo plugin

To implement the custom Studio logo, go directly to the file that exports the logo‘s React component. In this example, it‘s the default logo that comes from running sanity init plugin in a v2 Studio.

Before (Studio v2)

// plugins/my-studio-logo/Logo.js
import React from 'react'

const Logo = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="165.921"
    height="192.25"
    viewBox="0 0 155.551 180.235"
  >
    <path
      d="M211.608 74.824l-.033 103.562-89.703 51.752-89.67-51.81.032-103.56 89.704-51.754z"
      transform="matrix(.83022 0 0 .83324 -23.432 -15.35)"
      color="#000"
      fill="#16a085"
      stroke="#fff"
      strokeWidth="7.953"
      strokeLinecap="square"
    />
    <path
      d="M68.02 112.496c-.097 4.21.108 7.968-.884 11.814-1.54 5.958-5.164 7.2-11.53 6.576-2.118-.208-4.31-.54-5.257-3.144-.703-1.93-.922-3.49.607-4.83 1.367-1.2 1.395-2.44 1.06-3.918-.73-3.198-.546-6.43-.12-9.62.31-2.31.092-4.514-.198-6.802-.783-6.205-2.628-12.182-3.667-18.313-.567-3.358-.384-6.836-.814-10.258-.496-3.94-2.437-7.276-3.842-10.844-.247-.626-.655-.83-1.17-.388-.622.533-1.562 1.032-1.524 1.89.22 4.833-2.28 8.664-4.522 12.578-1.447 2.526-2.478 2.772-5.082 1.53-.842-.4-1.676-.83-2.468-1.318-1.007-.62-1.833-1.422-1.853-2.72-.048-2.918-.73-5.67-1.847-8.372-.412-1-.206-2.263-.24-3.407-.025-.72.284-1.566.006-2.143-1.15-2.387-.083-3.71 1.932-4.775 1.706-.9 2.866-2.23 3.57-4.123.898-2.41 2.58-4.143 5.07-5.23 1.877-.817 3.496-2.227 5.245-3.35.878-.562.826-1.248.57-2.16-.283-1.022-2.1-2.715.884-2.765.055 0 .166-.373.142-.56-.088-.69-.644-1.594.216-1.968 1.098-.477 1.627.674 2.224 1.303.782.824 1.456 1.764 2.09 2.713.992 1.485 2.03 2.88 3.688 3.722 1.52.773 2.59 2.134 3.62 3.406 2.615 3.222 5.205 6.484 7.522 9.92 4.802 7.12 11.895 10.15 19.95 11.32 7.096 1.028 14.248 1.678 21.38 2.466 1.658.183 3.298.145 4.97-.206 2.975-.624 4.166.504 3.634 3.38-.314 1.697.05 3.112 1.226 4.298 2.97 2.99 4.315 6.65 5.35 10.71 1.602 6.253 4.386 12.116 7.84 17.607 1.646 2.614 4.29 4.37 6.677 6.016 2.78 1.92 3.363 4.804 4.842 7.284.434.726.585 1.686 1.078 2.507 1.63 2.716 1.143 4.76-1.41 6.67-.415.313-.914.578-1.2.987-2.04 2.906-5.046 2.614-7.946 2.306-2.354-.25-3.158-1.713-2.32-3.953.362-.964.547-1.74-.127-2.604-.8-1.022-1.662-.5-2.523-.18-1.614.597-3.227.708-4.896.185-2.284-.714-3.07-1.822-2.46-4.104.408-1.535-.106-2.586-1.148-3.57-1.235-1.168-2.744-2.05-4.306-2.394-3.756-.83-5.526-3.763-7.61-6.448-2.71-3.485-5.384-7.003-8.235-10.368-1.013-1.196-2.625-1.57-4.268-1.65-3.306-.152-6.404.843-9.505 1.683-3.502.948-6.902 2.273-10.4 3.23-1.54.424-1.924 1.27-2.036 2.667-.218 2.708-.185 5.405.01 7.72z"
      fill="#fff"
    />
  </svg>
)

export default Logo

After (Studio v3)

Rename the Logo.js file to Logo.jsx (.tsx). You can also remove the React import, since Vite adds this automatically. We also recommend replacing the default export with a named export, since this can make it simpler to debug if something goes wrong.

// plugins/my-studio-logo/Logo.jsx
export const Logo = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="1rem" // your milage may vary
    height="1rem" // your milage may vary
    viewBox="0 0 155.551 180.235"
  >
    <path
      d="M211.608 74.824l-.033 103.562-89.703 51.752-89.67-51.81.032-103.56 89.704-51.754z"
      transform="matrix(.83022 0 0 .83324 -23.432 -15.35)"
      color="#000"
      fill="#16a085"
      stroke="#fff"
      strokeWidth="7.953"
      strokeLinecap="square"
    />
    <path
      d="M68.02 112.496c-.097 4.21.108 7.968-.884 11.814-1.54 5.958-5.164 7.2-11.53 6.576-2.118-.208-4.31-.54-5.257-3.144-.703-1.93-.922-3.49.607-4.83 1.367-1.2 1.395-2.44 1.06-3.918-.73-3.198-.546-6.43-.12-9.62.31-2.31.092-4.514-.198-6.802-.783-6.205-2.628-12.182-3.667-18.313-.567-3.358-.384-6.836-.814-10.258-.496-3.94-2.437-7.276-3.842-10.844-.247-.626-.655-.83-1.17-.388-.622.533-1.562 1.032-1.524 1.89.22 4.833-2.28 8.664-4.522 12.578-1.447 2.526-2.478 2.772-5.082 1.53-.842-.4-1.676-.83-2.468-1.318-1.007-.62-1.833-1.422-1.853-2.72-.048-2.918-.73-5.67-1.847-8.372-.412-1-.206-2.263-.24-3.407-.025-.72.284-1.566.006-2.143-1.15-2.387-.083-3.71 1.932-4.775 1.706-.9 2.866-2.23 3.57-4.123.898-2.41 2.58-4.143 5.07-5.23 1.877-.817 3.496-2.227 5.245-3.35.878-.562.826-1.248.57-2.16-.283-1.022-2.1-2.715.884-2.765.055 0 .166-.373.142-.56-.088-.69-.644-1.594.216-1.968 1.098-.477 1.627.674 2.224 1.303.782.824 1.456 1.764 2.09 2.713.992 1.485 2.03 2.88 3.688 3.722 1.52.773 2.59 2.134 3.62 3.406 2.615 3.222 5.205 6.484 7.522 9.92 4.802 7.12 11.895 10.15 19.95 11.32 7.096 1.028 14.248 1.678 21.38 2.466 1.658.183 3.298.145 4.97-.206 2.975-.624 4.166.504 3.634 3.38-.314 1.697.05 3.112 1.226 4.298 2.97 2.99 4.315 6.65 5.35 10.71 1.602 6.253 4.386 12.116 7.84 17.607 1.646 2.614 4.29 4.37 6.677 6.016 2.78 1.92 3.363 4.804 4.842 7.284.434.726.585 1.686 1.078 2.507 1.63 2.716 1.143 4.76-1.41 6.67-.415.313-.914.578-1.2.987-2.04 2.906-5.046 2.614-7.946 2.306-2.354-.25-3.158-1.713-2.32-3.953.362-.964.547-1.74-.127-2.604-.8-1.022-1.662-.5-2.523-.18-1.614.597-3.227.708-4.896.185-2.284-.714-3.07-1.822-2.46-4.104.408-1.535-.106-2.586-1.148-3.57-1.235-1.168-2.744-2.05-4.306-2.394-3.756-.83-5.526-3.763-7.61-6.448-2.71-3.485-5.384-7.003-8.235-10.368-1.013-1.196-2.625-1.57-4.268-1.65-3.306-.152-6.404.843-9.505 1.683-3.502.948-6.902 2.273-10.4 3.23-1.54.424-1.924 1.27-2.036 2.667-.218 2.708-.185 5.405.01 7.72z"
      fill="#fff"
    />
  </svg>
)

Add the logo component

Now you can import the Logo.jsx component directly to sanity.config.js and use the new studio.components.logo property:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
import schemas from './schemas/schema'
import deskStructure from './deskStructure'
import { Logo } from './plugins/my-studio-logo/Logo'
export default defineConfig({ title: "blog", projectId: "80ji1j7a", dataset: "production", plugins: [structureTool({ structure: deskStructure })], schema: { types: schemas, },
studio: {
components: {
logo: Logo
}
}
});

Document Actions

In the settings.js schema file, there is line that defines the available document actions for this document type:

// schemas/settings.js
export default {
  name: "settings",
  title: "Settings",
  type: "document",
__experimental_actions: ["update", "publish"],
fields: [ // ...all the fields ] }

You can delete this line (it doesn't do anything in Studio v3). Head over to sanity.config.js and add the following settings:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
import schemas from './schemas/schema'
import deskStructure from './deskStructure'
import { Logo } from './plugins/my-studio-logo/Logo'

export default defineConfig({
  title: "blog",
  projectId: "80ji1j7a",
  dataset: "production",
  plugins: [structureTool({
    structure: deskStructure
  })],
  schema: {
    types: schemas,
  },
  studio: {
    components: {
      logo: Logo
    }
  },
document: {
newDocumentOptions: (prev, { creationContext }) => {
if (creationContext.type === 'global') {
return prev.filter((templateItem) => templateItem.templateId != 'settings')
}
return prev
},
actions: (prev, { schemaType }) => {
if (schemaType === 'settings') {
return prev.filter(({ action }) => !['unpublish', 'delete','duplicate'].includes(action))
}
return prev
},
},
});

Now, the Settings document type should be gone from the global “create new” menu in the top left navigation bar, as well as the unpublish, delete, and duplicate actions in the document actions menu.

Add the Vision tool

Vision lets you run GROQ queries from a playground in your studio. To add the Vision plugin, you can import it in sanity.config.js and add it to the plugins array:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import schemas from './schemas/schema' import deskStructure from './deskStructure' import { Logo } from './plugins/my-studio-logo/Logo' export default defineConfig({ title: "blog", projectId: "80ji1j7a", dataset: "production", plugins: [ structureTool({ structure: deskStructure }),
visionTool()
], schema: { types: schemas, }, studio: { components: { logo: Logo } }, document: { newDocumentOptions: (prev, { creationContext }) => { if (creationContext.type === 'global') { return prev.filter((templateItem) => templateItem.templateId != 'settings') } return prev }, actions: (prev, { schemaType }) => { if (schemaType === 'settings') { return prev.filter(({ action }) => !['unpublish', 'delete','duplicate'].includes(action)) } return prev }, }, });

Render the Vision tool only in development

You might have noticed that the Vision tool was implemented like this in sanity.json for Studio v2:

"env": {
  "development": {
    "plugins": ["@sanity/vision"]
  }
},

For Studio v3 you can get the same behavior by passing a callback function to the tools property in the config object that filters out the Vision tool if the Studio runs in development (import.meta.envDEV == true) mode:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import schemas from './schemas/schema'
import deskStructure from './deskStructure'
import { Logo } from './plugins/my-studio-logo/Logo'

export default defineConfig({
  title: "blog",
  projectId: "80ji1j7a",
  dataset: "production",
  plugins: [
    structureTool({
      structure: deskStructure
    }),
    visionTool()
  ],
tools: (prev) => {
// 👇 Uses environment variables set by Vite in development mode
if (import.meta.env.DEV) {
return prev
}
return prev.filter((tool) => tool.name !== 'vision')
},
schema: { types: schemas, }, studio: { components: { logo: Logo } }, document: { newDocumentOptions: (prev, { creationContext }) => { if (creationContext.type === 'global') { return prev.filter((templateItem) => templateItem.templateId != 'settings') } return prev }, actions: (prev, { schemaType }) => { if (schemaType === 'settings') { return prev.filter(({ action }) => !['unpublish', 'delete','duplicate'].includes(action)) } return prev }, }, });

You can also pass a second argument that contains other useful contextual information. Let's say you want to load the Vision Tool only for administrators, and also in production mode, you can do the following:

// sanity.config.js
import { defineConfig } from "sanity";
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import schemas from './schemas/schema'
import deskStructure from './deskStructure'
import { Logo } from './plugins/my-studio-logo/Logo'

export default defineConfig({
  title: "blog",
  projectId: "80ji1j7a",
  dataset: "production",
  plugins: [
    structureTool({
      structure: deskStructure
    }),
    visionTool()
  ],
tools: (prev, context) => {
const isAdmin = context.currentUser.roles
.find(({ name }) => name === 'administrator')
if (isAdmin) {
return prev
}
return prev.filter((tool) => tool.name !== 'vision')
}, schema: { types: schemas, }, studio: { components: { logo: Logo } }, document: { newDocumentOptions: (prev, { creationContext }) => { if (creationContext.type === 'global') { return prev.filter((templateItem) => templateItem.templateId != 'settings') } return prev }, actions: (prev, { schemaType }) => { if (schemaType === 'settings') { return prev.filter(({ action }) => !['unpublish', 'delete','duplicate'].includes(action)) } return prev }, }, });

Setting up sanity.cli.js

You‘re almost done! To enable CLI commands that interact with your project, like deploying a GraphQL API, creating datasets, inviting users, etc., you need a configuration file for the CLI. Open sanity.cli.js, and add the following:

// sanity.cli.js
import {defineCliConfig} from 'sanity/cli'

export default defineCliConfig({
  api: {
    projectId: '<your-project-id>', // replace value with your own
    dataset: '<your-dataset-name>' // replace value with your own
  }
})

You can now run npx sanity [command] inside your project folder. Run npx sanity --help to see all the available commands.

Protip

You can add the -y flag to skip the npx installation prompt: npx -y sanity [command]

Clean up

Now you can delete the following:

  • The config folder (Note: there might be API tokens or similar that you want to bring over to your plugin configuration)
  • Any sanity.json file in your project

Stuck? Questions?

Hopefully, this guide, as well as the other documentation and migration guides, has helped you successfully refactor to Studio v3. However, if you‘re stuck, don‘t hesitate to let us know in the community. Remember to use 1 message + thread, and include any error message and available code to make it easier for us to debug.

Was this article helpful?