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 the guide’s use case: 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 and made a case for why it’s important, and 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 – with the main ones being spreadsheets, diagrams, and code. When you model content in Sanity, you’re not building a separate abstraction like a diagram or spreadsheet that you need to implement in code at a later time. You’re actually setting up:

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 from concept, to diagram, to code
  • The process is 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
  • You can version-control your model along the way: going from prototype all the way through to production without changing tools or platform

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, consider teaming up with someone who is for the implementation stuff. Modeling as a content+dev team can be real-time, flexible, and intuitive!

Protip

Understand the basics first

If you already know a bit about setting up Sanity schemas keep reading. If this is your first time using Sanity, take a peek at the following resources 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

You can then set up an account with Sanity and login with Google, Github, or your own e-mail and password. Sanity will now download all the necessary files and dependencies. 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

This may seem like a lot of stuff, granted, there is a ton of customization you can do with Sanity Studio. However, for this exercise, we will be doing all the modeling work in the schemas folder.

Protip

If you want to keep track of the way your content model changes over time, now is a good idea to set up version control like git in your project folder.

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

Navigate 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 into predefined ideas about how your content should be structured. We believe that content structure should be a unique response to your unique project needs. It’s the best way to provide durable content and workflows that deliver long term value.

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 a product content type. After all, what good is a candy subscription without products to put in it!

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.

The CandiCorp model will eventually 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 product.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
│  │  └── product.js // new
│  ├── objects // new
│  └── schema.js
├── static
├── tsconfig.json
└── yarn.lock

Let’s open product.js and add a title input field for the product, and make it a string field type:

// product.js

export default {
  title: 'Product',  // The human-readable label. Used in the studio.
  name: 'product',   // 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 provisioned our first 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 product from './documents/product' // new

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

Protip

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

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

Authoring a new product document in Sanity Studio.

Create your first taxonomy

With our first strokes applied to product, let’s make a category taxonomy for it. There are a few ways to achieve this with Sanity. What you choose depends on what you need. Let's explore what's on offer:

With editable strings

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

// product.js

export default {
  title: 'Product',
  name: 'product',
  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.

However, 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. Furthermore, there’s no single source of truth that binds the category values between product records: one candy product may have a value of gum ball, while another may have gum drop and the only way to know what’s in use is to query all the values for all records.

Protip

While this approach has no central place to manage terms, it can offer a lot when it comes to user-generated content.

If you’ve ever written a #hashtag you’re contributing to a folksonomy from which many others can benefit. This kind of social tagging has transformed the way large groups come together around ideas and terms.

With fixed values

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

// product.js

export default {
  title: 'Product',
  name: 'product',
  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 really 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 the next option.

Protip

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

We’ll talk more about objects in the next chapter!

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 product.

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
│  │  └── product.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 product from './documents/product'

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

And relate the two with an array of references:

// product.js

export default {
  title: 'Product',
  name: 'product',
  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:

  • Add as many categories as we like
  • Edit them outside of the schema code
  • Manage categories from a single location
  • Add them to product records

We also give ourselves the headroom to extend category.js with other fields 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 it’s important to consider where you situate them. While developers can query all references bi-directionally, you should think about the authoring experience and how it affects their workflows.

By adding categories as a reference in product.js you’re asking the editors to think about how their products should be categorized while they’re working on products.

You could also do the opposite by referencing an array of products from within category.js: asking editors to think of which products 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

The CandiCorp team weighed up their options for product categories and decided to create a new category.js document type and reference them as an array from product.js because:

  • They knew they needed structure and convention.
  • It was hard for them to commit to a definitive list of terms.
  • Both marketing and manufacturing stakeholders wanted to be able to add and edit categories.
  • They couldn’t yet figure out if a single category document would accommodate all their needs, but having the option to add additional structure and content features to category and/or product types were attractive to them.

What we’ve learned

We figured out how to set up the Sanity Studio with the command line interface and bootstrap a content model 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 you want to achieve.

What’s next?

In the next guide, we’ll build out all of CandiCorp’s main types in our content model and relate them to one another in meaningful ways. We’ll work through a range of juicy content modelling dilemmas that come with the terrain. Turning the world of business into durable content structures is a great challenge and a lot of fun.

Other guides by authors