August 12, 2020

Build schemas & taxonomies from scratch in Sanity.io

Official(made by Sanity team)

By Ronald Aveling & Knut Melvær

Build a content model from scratch with Sanity.io. Create your first custom type and learn how to think about taxonomies along the way.

This guide continues our series on content modeling with Sanity.io. In the previous chapter, we introduced CandiCorp​ – Norway’s manufacturer of the world’s tastiest and most interesting treats (at least in a parallel universe). Before that we talked about what content modeling is, made a case for why it’s important, and explained how it brings a ton of value to projects of all kinds.

If you’ve been eager to build structure the wait is over! We’ll now setup the foundations CandiCorp’s content model.

Why modeling content in Sanity is different

Content models can take shape in a variety of ways. When you model content in Sanity – you’re not building a separate thing that you need to implement in code at a later time, you’re setting up:

  • Actual fields and relationships
  • A real-time editing interface that you can immediately write to
  • A cloud-hosted datastore for all your content

If this sounds a bit daunting – it’s not as hard as you might think, and provides a lot of long term value because:

  • Setup takes minutes
  • You save time translating ideas, into diagrams, into code
  • Its fast and easy enough that editors and developers can sketch out ideas in real-time, and stress test them with real content as they go.

Setup your local Sanity modeling environment

Gotcha

We spent a lot of time making content modeling in Sanity easy and intuitive; but you do need to know the basics of creating objects and referencing files in JavaScript.

If you’re reading this as a content designer or strategist who is somewhat familiar with web development, it shouldn’t be too hard to follow along. (We’d like to think that writing complex Google Sheets and Excel functions are harder than setting up a Sanity schema.)

If coding’s not your thing, team up with someone who for all the implementation stuff. Modeling as a content+dev team can be real-time, flexible, and very productive.

Protip

If you’re familiar with Sanity schemas, keep reading. If this is your first time, take a peek at the following first:

Create a new Sanity project

Create a new directory called candicorp on your computer and move into it. Then initiate a new Sanity project using the following command in your shell application of choice (for example Terminal, or PowerShell):

~/candicorp

sanity init

Then set up an account with Sanity and login with Google, Github, or your own e-mail. Sanity will now download all the necessary stuff and in a few minutes, you should see a success message. Open your project folder in a code editor and you’ll see a structure like this:

├── README.md
├── config
├── node_modules
├── plugins
├── sanity-schema.txt
├── sanity.json
├── schemas // content model lives here
├── static
├── tsconfig.json
└── yarn.lock

We’ll be doing all the modeling work in the schemas folder.

Protip

Install version control like git in your project folder to track the way your content model changes over time.

Run the local environment

Sanity Studio comes with a development server that will automatically refresh the studio in the browser when you make edits to the content model. You start the local development environment with this command:

sanity start

In a few moments you should receive a success message and the local URL of your Sanity Studio:

Sanity Studio successfully compiled! Go to http://localhost:3333

Head over to that URL in your browser and log in with the same credentials that you set up your Sanity account with. It will load the Sanity Studio environment:

The Sanity Studio editing interface as a blank canvas.

The Empty Schema notice tells us that there are no document types set up, and this is exactly how we want it. Let’s do what the notice says and set up a new document type.

Protip

Sanity differs from other content management solutions in that it doesn’t lock you in to ideas about how your content should be structured. We believe your structure should be unique to your needs.

Create your first content type

In the previous chapter, we figured out a basic mental model for CandiCorp and explained the reasons behind how they got there. Let’s revisit the diagram again:

We'll start by building out an article content type. Within the schemas folder of our project there is a file called schema.js that comes with some handy comments on installation:

// schema.js

// First, we must import the schema creator
import createSchema from 'part:@sanity/base/schema-creator'

// Then import schema types from any plugins that might expose them
import schemaTypes from 'all:part:@sanity/base/schema-type'

// Then we give our schema to the builder and provide the result to Sanity
export default createSchema({
  // We name our schema
  name: 'default',
  // Then proceed to concatenate our document type
  // to the ones provided by any plugins that are installed
  types: schemaTypes.concat([
    /* Your types here! */
  ])
})

This file is where the entire content model comes together. We can add our own content types and fields at the bottom of that file. Modeling everything within schema.js is great for small projects, but we can also import references to other files and folders.

CandiCorp will have quite a few moving parts, so let’s set up a folder and the file-based way of handling the schema:

  1. Create a new directory called documents in /schemas.
  2. Create a new directory called objects in /schemas (we’ll cover this in the next guide)
  3. Create a file called article.js within the /schemas/documents.

Our project structure should now look like this:

├── README.md
├── config
├── package.json
├── plugins
├── sanity-schema.txt
├── sanity.json
├── schemas
│  ├── documents // new
│  │  └── article.js // new
│  ├── objects // new
│  └── schema.js
├── static
├── tsconfig.json
└── yarn.lock

Open article.js and add a title input field for the article, and make it a string field type:

// article.js

export default {
  title: 'Article',  // The human-readable label. Used in the studio.
  name: 'article',   // Required. The field name, and key in the data record.
  type: 'document',  // Required. The name of any valid schema type.
  // Input fields below, as many as you need.
  fields: [ 
    {
      title: 'Title',
      name: 'title',
      type: 'string',
    }
  ]
}

We’ve setup our first document and field, but we need to tell schema.js that it exists so it can build the content type in Sanity Studio.

// schema.js

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

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

import article from './documents/article' // new

export default createSchema({
  name: 'default',
  types: schemaTypes.concat([
    article, // new
  ])
})

Protip

You can review the complete list of schema types in our documentation.

Save the article.js and schema.js files. Your studio environment should now have a new artilcle ocument type that you can start to work with!

Authoring a new article document in Sanity Studio.

Create your first taxonomy

Let‘s make a category taxonomy for articles and other things. There are a few ways to do this with Sanity. What you choose depends on what you need:

With editable strings

We could make categories an array of strings that editors can add to each article, like so:

// article.js

export default {
  title: 'Article',
  name: 'article',
  type: 'document',
  fields: [
    {
      title: 'Title',
      name: 'title',
      type: 'string',
    },
    {
      title: 'Categories', // new
      name: 'categories',
      type: 'array',
       of: [{type: 'string'}],
       options: {
         layout: 'tags',
      }
    },
  ]
}

This approach is often used in “tag clouds” which connect a layer of meta content to the main content type. It's super flexible because the editor can add any value to the string field, and any number of strings to the array.

But its flexibility is also its downside: all that freedom makes it possible for editors to create a lot of ambiguity due to a lack of constraint. There’s also no single source of truth that binds the category values between article records, and the only way to know what’s in use is to query all the values for all records.

With fixed values

To manage candy categories centrally we could make the string array contain fixed values by adding a list of options:

// article.js

export default {
  title: 'Article',
  name: 'article',
  type: 'document',
  fields: [
    {
      title: 'Title',
      name: 'title',
      type: 'string',
    },
    {
      title: 'Categories', 
      name: 'categories',
      type: 'array',
      of: [{type: 'string'}],
      options: {
        list: [  // these values will be the only available options
          {value: 'hard-candy', title: 'Hard Candy'},
          {value: 'soft-candy', title: 'Soft Candy'},
          // etc
          ],
          layout: 'radio' // <-- defaults to 'dropdown' with a list of values
        },
    },
  ]
}

This solves the problem of ambiguity because the category values are set in stone. It's useful if your taxonomy is well established and unlikely to change a lot over time.

But (and there’s usually always a “but” when it comes to content modeling) because these values are set in code they’re not easy for editors to manage. If that’s important you may be better served by references.

Protip

To reuse a fixed list like this in other content types, place that field in an object type, so you can include it in any other object or document.

With a reference

We can strike a balance between flexibility and single-sourcing by creating a new content type file of category.js, and then establish a reference between it, and article.js.

Let's create the file:

// category.js

export default {
  title: 'Category',
  name: 'category',
  type: 'document',
  fields: [
    {
      title: 'Title',
      name: 'title',
      type: 'string',
    },
    // add a unique slug field for queries, permalinks etc
    {
      title: 'Slug',
      name: 'slug',
      type: 'slug',
      options: {
        // auto generates a slug from the title field
        source: 'title',
        auto: true
      }
    }
  ]
}

Put it in the schema folder:

├── schemas
│  ├── documents
│  │  └── article.js
│  │  └── category.js // new
│  ├── objects
│  └── schema.js

And add it to schema.js:

// schema.js

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

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

import category from './documents/category' // new
import article from './documents/article'

export default createSchema({
  name: 'default',
  types: schemaTypes.concat([
    article,
    category, // new
  ])
})

And relate the two with an array of references:

// article.js

export default {
  title: 'Article',
  name: 'article',
  type: 'document',
  fields: [
    {
      title: 'Title',
      name: 'title',
      type: 'string',
    },
    {
      title: 'Slug',
      name: 'slug',
      type: 'slug',
      options: {
        source: 'title',
      }
    },
    {
      title: 'Categories',
      name: 'category',
      type: 'array',
      of: [
        {
          type: 'reference',
          to: [
            {type: 'category'},
            // etc
          ]
        }
      ]
    },
  ]
}

Now we can create as many categories as we like, edit them in Sanity Studio, and connect to articles, and anything else for that matter. We can also extend category.js at a later time. This might come in handy for SEO category pages, or as section headings in CandiCorp’s print catalog.

Protip

When adding references, consider the editing experience when deciding where you place them.

Referencing categories within article.js asks the editor to think about how their articles should be categorized while they’re thinking about article content.

You could also do the opposite and reference an array of articles within category.js – asking editors to think of which articles best match the category.

Sometimes you need a tightly curated list of references, and other times you need infinite references. Each one is useful and leads to different results.

Example

CandiCorp decided to create a new category.js document type and reference categories within article.js because:

  • They knew they needed structure and convention.
  • They couldn’t decide on a fixed list of category terms up front.

What we’ve learned

We figured out how to bootstrap a content model in Sanity Studio from a blank canvas. We then created our first custom content type and provided it with a taxonomy for organisation.

Along the way, we explored different ways of handling taxonomies in Sanity.io, and talked about the importance of putting references in the right place depending on what we want to achieve.

What’s next?

We’ll build out all of CandiCorp’s main fields and relationships. We’ll work through a range of juicy content modelling dilemmas that come with the terrain.

Other guides by authors

Hierarchies, Graphs, and Navigation

Hierarchies are handy for organizing, but they can also fence you in. Learn how to build them, when to use them, and why you might want to treat navigation as a separate concern.

Go to Hierarchies, Graphs, and Navigation