DEV Community

Lukie Kang
Lukie Kang

Posted on

Figuring out Gatsby #4 - A detour to the world of Sanity.

In my previous posts I have discussed:

  1. Setting up Gatsby
  2. Making Pages
  3. Styling those pages

That is enough for a basic site, but if we need to make something a little more dynamic we need a:

  • Database to keep data that can be changed
  • Way of changing what's in the database
  • Way to show what we have in the database on our pages.

The cool kids call this a Backend, and this is something that one can dedicate a whole career to building out. We will not do this, we will use something called Sanity.

If you like using something else for data, that's cool, Gatsby doesn't really care too hard about what you use but for the app I am building, that's what I will be using. The concepts in later posts are pretty transferable.

What the hell is Sanity?

Sanity is a headless CMS. Headless just means that it doesn't have a frontend for users to see what's there, that's what we have Gatsby for. CMS stands for Content Management System which is a system that manages content 😸... fine, it's a tool that handles all ways we can change data, primarily... Create, Read, Update, Delete. AKA CRUD.

There is a whole bunch of headless CMSes on the market, some cater for simplicity, some for more control. Sanity is a good mix of the two, and the general principles here should work more or less whatever you use.

In a hypothetical restaurant app, we will have a bunch of dishes on the menu, menus change often. We don't want this to be a hassle, we might even want something that a semi-skilled client could manage without you. This is one reason why having a CMS to make this smooth is a good idea. However, it doesn't mean that setting it up is all that easy. So let's walk through it.

How do I get some of this Sanity?

You can probably figure most of it out by following the guidance on the Sanity Website.

I am going to focus on getting it up and running via the command line.

Step 1 - Install Sanity CLI and Initialise

This is as simple as typing npm install -g @sanity/cli && sanity init

This is will get it installed globally and the second command will initialise a blank Sanity instance in the location you are in.

You can check it is all version as intended with a sanity --version.

Once you have the starter files up and running, you weirdly should init again...

Step 2 - Reconfigure

It's time to introduce yourself to Sanity.

First, type sanity init --reconfigure and it will prompt you to create an account with Sanity (unless you have done this before). It's a fairly straightforward process that can use GitHub or Google as authentication providers to make it easier.

Second, give your project a name.

Thirdly it will ask if you want a private or public dataset and allows you to set up different datasets for testing etc. For your first time stick to the default which sets up a public production dataset but this is important to know for real life.

Step 3 - Intro to Sanity Studio

We have everything set up, so we can type npm start or sanity start and if all has gone well it will tell you Sanity is running on localhost:3333. If you open that in your browser, you will be prompted to logon and...

It will tell you that you have an empty schema. How dull.

We better go make something to look at here then.

Step 4 - Our first schema

The empty schema message will link to a guide. This a good step by step guide that provides more detail than I will type here. But I will go over what I did below.

Since I have a hypothetical restaurant site, I want to store content regarding the dishes that are served.

In the schemas folder, I create a new schema called dish.js. This file exports an object defining what a dish actually is. Hopefully, it makes sense with my comments:

export default {
  name: 'dish', // what sanity will know it as
  title: 'Dishes', // what we see it called in Sanity Studio
  type: 'document', // We will get to this later, document will do for now
  fields: [
    // what fields does a dish have?
    {
      name: 'name', // What Sanity will know it as
      title: 'Dish Name', // What we see it called in Sanity Studio
      type: 'string', // The datatype, this could be lots of things.
      description: 'Name of the Dish', // Just explains what the field is.
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Once we have the dish schema set up, we need to add it to the overall schema.js file with a import dish from './dish and modifying the createSchema object as follows:

types: schemaTypes.concat([dish]),

Hopefully, you begin to see how multiple schemas can be built up in this fashion.

Step 5 - Bask in our incredible Back End skills

Let's hop back to Sanity Studio. If all has gone well we can see we have Dishes on the left-hand side. But no content. We can fix that by clicking the new icon in the right-hand side and add our first Dish!

Fish and Chips
Of course, it isn't all that interesting just storing the names of dishes, so once you are happy it works as intended you can delete it via the menu on the bottom right-hand side.

Let's make it better!

Well, the possibilities are limitless at this point. The Sanity docs are really good for figuring out how to build upon it but here is what I did:

import { check } from 'prettier';

export default {
  name: 'dish',
  title: 'Dishes',
  icon: () => '🥣', // Lets give it a cool icon!
  type: 'document', //
  fields: [
    {
      name: 'name',
      title: 'Dish Name',
      type: 'string',
      description: 'Name of the Dish',
    },
    {
      name: 'vegetarian',
      title: 'Vegetarian',
      type: 'boolean',
      description: 'Is it Vegetarian?',
      options: {
        layout: 'checkbox',
      },
    },
    {
      name: 'vegan',
      title: 'Vegan',
      type: 'boolean',
      description: 'Is it Vegan?',
      options: {
        layout: 'checkbox',
      },
    },
    {
      name: 'price',
      title: 'Price',
      type: 'number',
      description: 'Price of dish in pence',
      validation: (Rule) => Rule.min(99).max(10000), // Limits what can be entered for price
    },
    {
      name: 'image',
      title: 'Dish Image',
      type: 'image',
      options: {
        hotspot: true, // clever thing that lets us edit where to focus the picture when resizing
      },
    },
    {
      // Add a slug to deal with pesky spaces in names
      name: 'slug',
      title: 'slug',
      type: 'slug',
      options: {
        source: 'name',
        maxLength: 50,
      },
    },
  ],
  preview: {
    select: {
      name: 'name',
      vegetarian: 'vegetarian',
      vegan: 'vegan',
    },
    prepare: (fields) => ({
      title: `${fields.name} ${
        fields.vegan ? '- 🌱  Ve' : fields.vegetarian ? '-🍆 Veg' : '' //Nesting Ternaries is a naughty thing to do btw.
      }`,
    }),
  },
};

Enter fullscreen mode Exit fullscreen mode

Which results in:

Fish and Chips

Starting to look tasty! Note that we:

  • Added Vegan and Vegetarian Options (and somehow I decided Fish and Chips was vegetarian)
  • Added a preview property that lets us get some of the info without having to go into it.

But let's start getting even more clever and look at some related data.

Relational Data with a Second Content Type

Ok cool, we have some dishes in our restaurant but we don't want our customers with special dietary requirements to be worried about what we are serving up so we want to capture data around the use of dairy, nuts, gluten. That sort of thing.

Multiple dishes use dairy products and maybe later we could filter out dairy products from the menu. To do this and save effort in the long run. it needs to exist as a separate content type rather than just be a piece of info buried within each dish.

A dish can fit many intolerances, i.e it could have Gluten and Shellfish. So we call this a one to many relationships as ONE dish may have MANY ingredients that we care about.

Ok so first we need a new schema, called intolerance.js and we want to keep it fairly simple at first:

export default {
  name: 'intolerance',
  title: 'Dietary Intolerances',
  icon: () => '⚠️',
  type: 'document',
  fields: [
    {
      name: 'name',
      title: 'Name',
      type: 'string',
      description: 'Name of the Dietary Tolerance',
    },
  ],
};

Enter fullscreen mode Exit fullscreen mode

Don't forget to add to the schema.js file as before:

types: schemaTypes.concat([dish, intolerence]),

Linking it up

Now that we have done that we need to link dishes to their intolerances. This requires us to make an addition to the pizza schema:

  {
      name: 'Intolerences',
      title: 'Contains',
      type: 'array',
      of: [{ type: 'reference', to: [{ type: 'intolerance' }] }],
    },
Enter fullscreen mode Exit fullscreen mode

So what are we doing here? We are adding an array to our dish schema, and then we are saying that the array will contain references to our intolerances. This results in the following:

Fish and Chips

Its a bit fiddly in the UI but allows us to link our previously created intolerances to our item. This isn't the most complex data ever for the sake of a short blog post but it's easy to see how this can be developed.

My final (At least for now) dish schema looks like this:

export default {
  name: 'dish',
  title: 'Dishes',
  icon: () => '🥣', // Lets give it a cool icon!
  type: 'document', //
  fields: [
    {
      name: 'name',
      title: 'Dish Name',
      type: 'string',
      description: 'Name of the Dish',
    },
    {
      name: 'vegetarian',
      title: 'Vegetarian',
      type: 'boolean',
      description: 'Is it Vegetarian?',
      options: {
        layout: 'checkbox',
      },
    },
    {
      name: 'vegan',
      title: 'Vegan',
      type: 'boolean',
      description: 'Is it Vegan?',
      options: {
        layout: 'checkbox',
      },
    },
    {
      name: 'price',
      title: 'Price',
      type: 'number',
      description: 'Price of dish in pence',
      validation: (Rule) => Rule.min(99).max(10000), // Limits what can be entered for price
    },
    {
      name: 'image',
      title: 'Dish Image',
      type: 'image',
      options: {
        hotspot: true, // clever thing that lets us edit where to focus the picture when resizing
      },
    },
    {
      // Add a slug to deal with pesky spaces in names
      name: 'slug',
      title: 'slug',
      type: 'slug',
      options: {
        source: 'name',
        maxLength: 50,
      },
    },

    {
      name: 'Intolerences',
      title: 'Contains',
      type: 'array',
      of: [{ type: 'reference', to: [{ type: 'intolerance' }] }],
    },
  ],
  preview: {
    select: {
      name: 'name',
      vegetarian: 'vegetarian',
      vegan: 'vegan',
    },
    prepare: (fields) => ({
      title: `${fields.name} ${
        // eslint-disable-next-line no-nested-ternary, naughty naughty
        fields.vegan ? '- 🌱  Ve' : fields.vegetarian ? '-🍆  Veg' : ''
      }`,
    }),
  },
};

Enter fullscreen mode Exit fullscreen mode

Customising our Inputs

One last thing, mostly optional. We are not limited to how Sanity renders the inputs by default. We can make our own version of the inputs using React components, we just need to tell Sanity to do that.

The basics

First, we want to create a components folder and inside of that the component we want to use.

We create a simple React component and import it into the schema as we would in React. In the relevant field object add the following property to tell it to use the component for the input:

inputComponent: nameOfComponent

Its a good idea to get a basic component rendering something basic to check it works before moving on.

Actually customising the input

There are a few things that are special because we are using Sanity in order for our input to talk to it properly.

1. Get Props from Sanity

This is Because we told Sanity that we want to use this component for the input, it gives us a whole bunch of Props we have access to that relate to the data in Sanity. Some key ones are:

  • Type, the filed object, contains the title and description and other things
  • Value, what is in the input
  • onChange, what to do when it changes
  • inputComponent, reference to input itself so Sanity knows what to look at.

2. Setting up Patching function

Sanity has its own method of updating its data, and it requires some Santity magic as follows:

  1. We import the following:

import PatchEvent, {set, unset} from 'part:@sanity/form-builder/patch-event'

  1. Build the following function:
function createPatchFrom(value){
  PatchEvent.from(value === '' ? unset() : set(value))
}
Enter fullscreen mode Exit fullscreen mode

Basically, Sanity will set the value if the input value exists and unset it if it does not.

3. Build our input and complete our component

So I am building an input for an employee's age here, which is a bad idea for real life but for the sake of showing you the whole component:

import React from 'react'
import PatchEvent, {set, unset} from 'part:@sanity/form-builder/patch-event'

function createPatchFrom(value){
  PatchEvent.from(value === '' ? unset() : set(value))
}

export default function AgeInput({ type, value, onChange, inputComponent }){
  return (
    <div>
      <h2>{type.title} {value ? value : ''</h2> 
      <p>{type.description}</p>
      <input
        type={type.title}
        value={value}
        onChange={event => onChange(createPatchFrom(event.target.value)})
        ref={inputComponent}
      />
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

Phew, if you don't care about Sanity this would be a dull one but since Gatsby doesn't involve itself in backend stuff we need something to handle our data. Sanity is a good pick for something fairly easy to work with and will be used in my posts going forward.

Next time, we will next get to look at hooking up our Gatsby to our Sanity so we can see the fruits of our hard work on the backend.

Top comments (0)