Sanity Studio v3

Den här artikeln är inte översatt till svenska och visas därför på engelska istället.


In this article we go through how to setup a Sanity Studio v3 project as well as a plugin project complete with our own custom typings, and link them together.

Uppskattad lästid : 14 minuter

Gå till avsnitt

Key takeaways

  • New build tools work mostly fine
  • TypeScript support in Sanity Studio v3 is great, but requires some configuration

Prerequisites

The following required tooling needs to be installed for this article:

  • NodeJS
  • Yarn

This article assumes a basic knowledge of TypeScript and React. It does not go in-depth on how Sanity Studio works - prior experience here, while not necessary, is helpful to understand the contents of the article.

Introduction

Sanity has recently made Sanity Studio v3 generally available, which comes with a much better TypeScript coverage and updated API's. Let's take a look at how we can initialize a new Sanity Studio project and create a simple but fully typed plugin for it.

The scenario

The built-in image type in Sanity Studio is a bit limited for web use. On websites you want for instance your img elements to contain an alternate text, called an "alt text". Let's create a plugin for Sanity Studio where we extend the built-in image type with an additional altText field.

The Sanity Studio project

To begin, let's create two separate projects. One of the projects will contain our Studio plugin, and one will be a barebones Sanity Studio project where we will link in our plugin.

First, we need to install the Sanity v3 CLI. In a terminal, run the following command:

yarn global add sanity@latest

Next, create the Studio project. Let's just call it "sanity-v3-blog":

yarn create sanity --create-project sanity-v3-blog --dataset production --template clean

You'll be prompted for a project output path. You can just press enter here to use the default value.

When prompted for TypeScript, choose Yes.

When prompted for package manager, choose yarn.

Next, navigate to the newly created project folder and try starting the studio.

cd sanity-v3-blog
yarn dev

Now, if everything works well and you're a developer like me the first thing you may notice in the Studio is its dark theme, assuming that you have configured dark mode as the default theme for your operating system. Other than that the actual looks of the Studio hasn't changed much, so let's go take a look at the project structure. I firstly recommend creating a new folder called "src" and moving the generated "schema" folder into it, to keep the project nice and tidy.

Next have a look inside "sanity.config.ts" - this is the new config file for Sanity that replaces the old "sanity.json" file. Here the exported (empty) schema types from "src/schemas/index.ts" should already be added. You may need to update the import here if your IDE didn't manage it automatically when we moved the file. Later we'll add a reference here to our upcoming plugin.

import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
import {visionTool} from '@sanity/vision'
import {schemaTypes} from './src/schemas'

export default defineConfig({
  name: 'default',
  title: 'sanity-v3-blog',

  projectId: '5234zuar',
  dataset: 'production',

  plugins: [deskTool(), visionTool()],

  schema: {
    types: schemaTypes,
  },
})

Now let's create a simple document schema type "blogArticle" which will hold our image. For the schema type, let's use a couple of the new type definition helper functions from Sanity: "defineType" and "defineField". Create the file "blogArticle.ts" inside the "src/schemas" folder and add the following content:

import {defineField, defineType} from 'sanity'

export const blogArticle = defineType({
  name: 'blogArticle',
  type: 'document',
  title: 'Blog article',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      title: 'Title',
    }),
    defineField({
      name: 'slug',
      type: 'slug',
      title: 'Slug',
      options: {
        source: 'title',
      },
    }),
    defineField({
      name: 'image',
      type: 'image',
      title: 'Image',
    }),
  ],
})

Since we use the "define*" functions here, the schema type is now actually fully typed. Try it out! If you try adding a random property to one of the fields for instance, you'll receive TypeScript errors if the property definition doesn't exist on the field. Similarly you gain intellisense auto-completion on each property in the schema type.

Next, add our new document schema to "src/schemas/index.ts" so that we can start creating some documents:

import { blogArticle } from "./blogArticle";

export const schemaTypes = [blogArticle]

Now you can run "yarn dev" if you haven't already, open up your browser and create a new blog article document in the studio. The editing experience should be quite similar to previous versions of the studio.

The plugin project

Now let's create our "web image" plugin. First, you need to install "npx" if you haven't already:

yarn global add npx

Next, navigate out from the newly created sanity project and create the plugin project:

cd ..
npx @sanity/plugin-kit init sanity-plugin-web-image
cd sanity-plugin-web-image

You can enter any values you like when prompted for Author name, etc. When prompted for package manager though, choose yarn.

This is where the new tooling gets a bit hairy. Usually I would try out the plugin project by running "yarn build" in this step. Doing this results in a build error though with "@sanity/pkg-utils" version 2.0.0, which didn't happen in previous versions. Luckily the error message is straight-forward though:

C:\Projects\sanity-plugin-web-image  (sanity-plugin-web-image@1.0.0)
λ yarn build
yarn run v1.22.5
$ yarn run clean && plugin-kit verify-package --silent && pkg-utils
$ rimraf lib
[error] `type` in `./package.json` must be of type 'commonjs' | 'module' (received undefined)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

It says " `type` in `./package.json` must be of type 'commonjs' | 'module' (received undefined)". Okay, let's try to set the "type" field in package.json to "commonjs".

{
  ...
  "type": "commonjs",
  ...
}

Now "yarn build" should work, and the output should show up in the "lib" folder.

Before we add our new image schema type, let's tweak a few configuration options. I like to start by going to "package.config.ts" and setting its "minify" property to false, which makes the built JavaScript files non-minified. Just for slightly easier debugging. Next, I recommend going to ".eslintrc" and removing the line with "plugin:prettier/recommended" and turning off the "linebreak-style" rule. I found that config line interfering with my ability to use Windows style line endings, which I would like to keep since git automatically converts them to Unix style and vice versa. The resulting .eslintrc should look like:

{
  "root": true,
  "env": {
    "node": true,
    "browser": true
  },
  "extends": [
    "sanity",
    "sanity/react",
    "sanity/typescript",
    "plugin:react-hooks/recommended"
  ],
  "rules": {
    "linebreak-style": "off"
  }
}

Now, let's add some files and folders. Create the following:

  • src/schema folder
  • src/schema/webImage.ts
  • src/schema/types.ts
  • src/modules.d.ts

In "webImage.ts", add the following schema type:

import {defineField, defineType} from 'sanity'

export const webImage = defineType({
  name: 'webImage',
  type: 'image',
  title: 'Image',
  fields: [
    defineField({
      name: 'altText',
      type: 'string',
      description: 'Providing an alt text is recommended to help screen readers assist visually impaired visitors and to help search engines rank the website.',
      title: 'Alt text',
    }),
  ],
})

It's a pretty standard schema type inheriting from the base "image" schema type, with an added "altText" field. Now let's add some custom typings for it, so that usages of the schema type will get typed! In "types.ts", add the following:

import {ImageDefinition, ImageValue, InitialValueProperty, RuleDef, ValidationBuilder} from 'sanity'

export interface WebImageValue extends ImageValue {
  altText?: string
}

export interface WebImageRule extends RuleDef<WebImageRule, WebImageValue> {}

export type WebImageDefinition = Omit<ImageDefinition, 'type' | 'initialValue' | 'validation'> & {
  type: 'webImage'
  initialValue?: InitialValueProperty<any, WebImageValue>
  validation?: ValidationBuilder<WebImageRule>
}

Here we again inherit from the base image type, this time with TypeScript interfaces. We override the base "initialValue" and "validation" fields with our custom WebImageValue/WebImageRule respectively, to provide better intellisense when using these fields.

To bring these new typings to the bundle built by "yarn build", we need to export them from "src/index.ts". Edit index.ts as the following:

import {definePlugin} from 'sanity'
import {webImage} from './schema/webImage'

export interface WebImageOptions {
  /* nothing here yet */
}

/**
 * ## Usage in sanity.config.ts (or .js)
 *
 * ```
 * import {defineConfig} from 'sanity'
 * import {webImagePlugin} from 'sanity-plugin-web-image'
 *
 * export const defineConfig({
 *     //...
 *     plugins: [
 *         webImagePlugin()
 *     ]
 * })
 * ```
 */
export const webImagePlugin = definePlugin<WebImageOptions | void>((options = {}) => {
  // eslint-disable-next-line no-console
  console.log('hello from sanity-plugin-web-image')

  return {
    name: 'sanity-plugin-web-image',
    schema: {
      types: [webImage],
    },
  }
})

export * from './schema/types'

Finally, we need to add a TypeScript module augmentation file where we merge Sanity's builtin schema types interface with our new schema type. In modules.d.ts, add the following:

import {WebImageDefinition} from './schema/types'

declare module 'sanity' {
  export interface IntrinsicDefinitions {
    webImage: WebImageDefinition
  }
}

With this file we can add our webImage schema type as fields to other schema types, and the typings will "just work" as long as we keep using "defineType" and "defineField". Unfortunately the contents of this file doesn't currently get bundled with the rest of our custom types, so we will have to re-create it inside our studio project.

Now, build the project again, then run the link-watch command:

yarn build
yarn link-watch

Here you may encounter another error when running "yarn link-watch": "Error: missing package.json". Something about the "watch" part of the command doesn't work properly at the moment, but it's fine as it's not necessary to use the plugin. After running the commands you should see something about navigating to the studio project and running "yalc" in the terminal. Since we have npx installed we don't need to globally install yalc, we can just run it through npx.

Putting it all together

Now, navigate to the studio project you setup earlier. Then run the following command:

npx yalc add --link sanity-plugin-web-image && yarn install

This should link the plugin to your studio project as a local NPM package, without having to publish the plugin to npmjs.com. Next, reference the plugin in sanity.config.ts:

import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
import {visionTool} from '@sanity/vision'
import {schemaTypes} from './src/schemas'
import {webImagePlugin} from 'sanity-plugin-web-image'

export default defineConfig({
  name: 'default',
  title: 'sanity-v3-blog',

  projectId: '5234zuar',
  dataset: 'production',

  plugins: [deskTool(), visionTool(), webImagePlugin()],

  schema: {
    types: schemaTypes,
  },
})

Now, remember that the module augmentation wasn't bundled with our plugin? Therefore we need to create a "modules.d.ts" file again in our studio project to get the correct typings:

import {WebImageDefinition} from 'sanity-plugin-web-image'

declare module 'sanity' {
  export interface IntrinsicDefinitions {
    webImage: WebImageDefinition
  }
}

Lastly, replace the image field in "blogArticle" with our brand new "webImage" schema type:

import {defineField, defineType} from 'sanity'

export const blogArticle = defineType({
  name: 'blogArticle',
  type: 'document',
  title: 'Blog article',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      title: 'Title',
    }),
    defineField({
      name: 'slug',
      type: 'slug',
      title: 'Slug',
      options: {
        source: 'title',
      },
    }),
    defineField({
      name: 'webImage',
      type: 'webImage',
      title: 'Image',
      initialValue: { 
        altText: "Example initial alt text",
      }
    }),
  ],
})

As an example I added an "initialValue" object to the field. Here we get intellisense for the regular image values such as "asset", "crop", and "hotspot", but also our new field "altText".

Conclusion

Working with Sanity Studio v3 has some quirks as it is still quite a new release, which will hopefully only get better as time goes on. For now though the added TypeScript support is a great feature that really makes me want to go back to older projects and migrate them to the new version.