Keplux logo.

Content modeling with Sanity schemas

Learn how to create custom schemas for a blog with Sanity.

By Chris Jardine

January 25, 2023

A desk set up for blogging.

In this tutorial, you'll learn how to create various schemas that serve as the data models for a blog. This will be very basic, using Sanity's built-in 'block' type without any customizations. If you're looking for information on creating custom content blocks, such as for embedding YouTube videos or code blocks, you can find that here: [LINK TO ARTICLE].

Initialize the project

If you're following along in the series, you should already have a project. If not, the starter project can be found here. If you have your own existing Sanity project, you shouldn't have any issues following this tutorial either.

Clone the starter project if you need to:

git clone https://github.com/c-jardine/typescript-next-sanity-v3.git blog
cd blog
npm i

Set up the project structure

We'll be keeping our schemas in the src/studio directory.

Schemas are built with a combination of 'documents' and 'objects'. Documents are used for the main elements, such as posts, authors, tags, etc. Objects can be used inside of documents as a way to reuse fields, provide additional configurations, and more.

To start, we'll create a documents directory inside the studio directory. In that, create a file called author.ts. That will, of course, be the schema for authors.

Build the author schema

You can use whatever fields you'd like your author schema to have. I'm just going to start with a name, avatar, and bio.

We'll define our schema with the defineType function provided by the sanity package. This function takes in an object with our schema definition:

author.ts
import { defineType } from 'sanity';

const author = defineType({
  // Your schema definition here
});

export default author;

ts

At a minimum, our schema must have a name, type, and fields array. Since we're creating a document, the type property will be 'document'. Click here to learn more about the document type.

The name property must be unique, as this is what the property name will be in the API response. We'll just use 'author'.

I'm also going to add the property title and give it the value 'Authors'. This will be shown as the title of the document type in the Studio. Without it, the title will be generated automatically from the name property.

The fields array will contain all of the fields (crazy, I know) we want to provide data for in the Studio.

Create author fields

For a list of available field types, click here.

Inside the fields array, we can create objects for the data we want our authors to have. Each object requires name and type properties.

For the first object, we'll create name field of type string. Note: Sanity has a string type and a text type. Use string for shorter fields as it will generate a single-line text input. The text type will generate a textarea input, so it's better suited for shorter values.

Follow the same process for the 'bio' field, using the text type.

Avatar and the Image type

Read more about the image type here.

The avatar field will use the type image. This type comes with additional options. We want to be able to crop our images, so we'll add the hotspot: true option.

Wrapping up the author schema

With all of our fields populated, here is what it should look like now:

author.ts
import { defineType } from 'sanity';

const author = defineType({
  title: 'Author',
  name: 'author',
  type: 'document',
  fields: [
    {
      title: 'Name',
      name: 'name',
      type: 'string',
    },
    {
      title: 'Bio',
      name: 'bio',
      type: 'text',
    },
    {
      title: 'Avatar',
      name: 'avatar',
      type: 'image',
      options: {
        hotspot: true,
      },
    },
  ],
});

export default author;

ts

If you run your app and go to your Studio, you should now see an Authors field that lets you create new authors.

Now, we're missing a couple important things. We want to be able to access our authors on our website in a readable way, such as at http://localhost:3000/authors/chris-jardine. To do this, Sanity includes a special slug type, which we can use to generate a url-encoded string from any field we'd like (or you can enter it manually). I like to generate it from the name field. To make sure someone can't create an author without a name, we'll add validation to that field.

Adding field validation

Read more about validation here.

Say we want to require an author's name field to not be empty and between 4 and 28 characters. We can implement that like so:

author.ts
// ...
{
  title: 'Name',
  name: 'name',
  type: 'string',
  validation: Rule => Rule.required().min(4).max(24)
},
// ...

ts

Adding the slug field

Read more about the slug field here.

We'll need to add a new field of type slug. This object will also use source: 'name' as an option. This means the slug will be generated from the 'name' field. Personally, I like to add it as the first object in the fields array, but feel free to organize your fields any way you'd like.

author.ts
// ...
{
  title: 'Slug',
  name: 'slug',
  type: 'slug',
  options: {
    source: 'name',
  },
},
// ...

ts

Build the post schema

Create a new document file called post.ts. We'll keep this simple for the sake of this tutorial. I want to add a slug, title, author, and content body. The slug and title should be easy:

post.ts
const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    {
      title: 'Slug',
      name: 'slug',
      type: 'slug',
      options: {
        source: 'title',
      },
    },
    {
      title: 'Title',
      name: 'title',
      type: 'string',
      validation: (Rule) => Rule.required(),
    },
  ],
});

export default post;

ts

Create a relation to the author schema

We want to use authors that exists in our database, so this part is a little different. To do this, we'll use the type reference, which requires another property named to, which will tell Sanity what type to use for the relation. In our case, we want to say the field has a reference to author. We do that like so:

post.ts
// ...
{
  name: 'author',
  title: 'Author',
  type: 'reference',
  to: [{ type: 'author' }],
  validation: (Rule) => Rule.required(),
},
// ...

ts

Dealing with rich text and block content

Sanity's built-in editor rich text and block content editor is great for writing blogs. That's what I'm using to write this blog. It utilizes portable text, which makes it super easy to present our blog on the front end however we want.

It's also highly customizable. The default editor supports standard HTML headings, blockquotes, bold, italic, underline, strikethrough, code, ordered and unordered lists, and links. However, you can build custom types for the editor. For example, this blog's code blocks with syntax highlighting and tab groups are custom types I created. It's pretty straightforward to do, as the presentation you see is completely decoupled from the schema itself.

Anyway, to use the built-in editor, your field will use the type array, which requires an additional of property whose value is an array of types. We just want to use the type block.

Note: The of field is where you'll add custom content types.

So, that field will look like this:

post.ts
// ...
{
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [{ type: 'block' }],
  validation: (Rule) => Rule.required(),
},
// ...

ts

Final schemas

You should have ended up with the following in your schema files:

author.ts
import { defineType } from 'sanity';

const author = defineType({
  title: 'Author',
  name: 'author',
  type: 'document',
  fields: [
    {
      title: 'Slug',
      name: 'slug',
      type: 'slug',
      options: {
        source: 'name',
      },
    },
    {
      title: 'Name',
      name: 'name',
      type: 'string',
      validation: (Rule) => Rule.required().min(4).max(28),
    },
    {
      title: 'Bio',
      name: 'bio',
      type: 'text',
    },
    {
      title: 'Avatar',
      name: 'avatar',
      type: 'image',
      options: {
        hotspot: true,
      },
    },
  ],
});

export default author;

ts

Configure Sanity to use new schemas

Before we can use our schemas, we need to add them to the schemaTypes array in the src/studio/schemas/index.ts file.

I like to create separate arrays for documents and objects to help clean up the code a little bit. I also use barrel files a lot, so your imports may look a little different. Here's what my file looks like:

studio/schemas/index.ts
import { author, post } from './documents';

const documents = [author, post];

export const schemaTypes = [...documents];

ts

This file should already be imported in your sanity.config.ts file, so you won't have to do anything else. But if you're having issues, it should look like this:

sanity.config.ts
import { defineConfig } from 'sanity';
import { schemaTypes } from './schemas';

const config = defineConfig({
  // ...other config settings
  schema: {
    types: schemaTypes,
  },
});

export default config;

ts

Create your blog content

At this point, you can start up your server if it isn't already running and check out your Sanity Studio. You should see something resembling the following:

Sanity Studio showing the Author and Post data models.

Sanity Studio showing the Author and Post data models.

You can now create your authors and blog posts!

Summary

In this tutorial, we covered how to create basic schemas for modeling your data with Sanity. We also covered relations and block content. The process for creating schemas for anything follows this same process, so you should pretty much be able to model anything you want.

More info

  • Sanity schema types

  • Sanity block type

  • Block content and portable text

Latest posts

Configuring ESLint and Prettier in Next.js with TypeScript

December 4, 2022