Guide

Getting started with Structure Builder

Structure builder lets you override the default document lists in the Studio, and gives full flexibility to how documents are grouped.

Knut Melvær

Knut runs developer relations at Sanity.io.

With Structure Builder you can override the default behavior for how the Sanity Studio lists out documents. Without any configuration, the Studio will list out your document types in the leftmost pane, a list of the documents under each type in the second, and the editor for a selected document in the third. In this guide, you’re introduced to some common use cases and central concepts for Structure Builder.

You might also want to check out the introduction and the reference documentation.

We’re using the portfolio starter for this guide, but you should be able to tag along using your own project. All the code snippets should be copy-pasteable, but we recommend that you try typing it out to get a better feel for how the code are structured. Structure Builder is written using TypeScript, so if you have a compatible code editor (like VS Code), you should be able to get auto-complete for the different methods. It should be noted that this tutorial uses regular JavaScript, and you don't need to know TypeScript in order to use this feature (or any feature in Sanity Studio).

The Studio comes a default structure out of the box. In order to override the default behaviour of how the documents are grouped and listed out, we have to tell the Studio that it should look for the structure elsewhere. This is done by leveraging the parts system. In this case you have to add an entry to the parts array in sanity.json, located at the root level of your studio folder. That will let the Studio know where your configuration file is.

{
  "root": true,
  "project": {
    "name": "porfolio"
  },
  "api": {
    "projectId": "projectId",
    "dataset": "dataset"
  },
  "plugins": [
    "@sanity/base",
    "@sanity/components",
    "@sanity/default-layout",
    "@sanity/default-login",
    "@sanity/dashboard",
    "@sanity/desk-tool",
    "dashboard-widget-structure-menu",
    "dashboard-widget-document-list",
    "dashboard-widget-netlify"
  ],
  "parts": [
    {
      "name": "part:@sanity/base/schema",
      "path": "./schemas/schema.js"
    },
{
"name": "part:@sanity/desk-tool/structure",
"path": "./deskStructure.js"
},
{ "implements": "part:@sanity/dashboard/config", "path": "./dashboardConfig.js" } ] }

Now you have to create and save a new file to the path you specified in sanity.json. In this case we have called it deskStructure.js and we have saved the file on root in the studio folder.

If you restart the local development server (ctrl + C and > sanity start), you will get an error message. This is because you haven’t given the Studio any structure yet. Let’s begin defining the default structure that reproduces the default behavior:

import S from '@sanity/desk-tool/structure-builder'

export default () =>
  S.list()
    .title('Content')
    .items(
      S.documentTypeListItems()
    )

First we have to import the Structure Builder, this is a collection of methods that lets you define how the panes and lists in the Studio should work. We use the single S here mostly for brevity.

The S.list() defines the first pane. The .title('Content) its title, and .items() the content of the list. S.documentTypeListItems() is a “convenience method" that returns a list of the document types we have defined in schema.js. We will return to how we can filter out those document types we don't want in this first pane.

How to make a single document (a.k.a a singleton)

A common use case for structure builder is restricting a document type to only having one document. This is for when you want to make a configuration document, some global navigation, or some singular metadata.

In the portfolio starter we want some settings for the site with the website’s title, description, some keywords and an author. We have defined this as a document type with the name siteSettings and imported it to schema.js. For the time being, it doesn't make sense to have multiple site settings (but it's great to know that you can in the future!), so let's make it so that there is a list item called ”Settings,” and when the editor push it, it will open the editor with the fields.

import S from '@sanity/desk-tool/structure-builder'

export default () =>
  S.list()
    .title('Content')
.items([
S.listItem()
.title('Settings')
.child(
S.editor()
.schemaType('siteSettings')
.documentId('siteSettings')
),
...S.documentTypeListItems() ])

Let's go through the Settings configuration step by step:

  • .items() takes an array. Since we're adding more array items here, we have to add the array brackets ([]), and spread (...) the S.documentTypeListItem() into it.
  • Next up is the S.listItem().title('Settings') which creates the top item in the first pane.
  • To define what should happen when you click it, you need to give it a .child().
  • Inside the .child() we put the S.editor(), which tells the Studio that when you click on the item with the title "Settings", it should return an editor in the next pane.
  • To specify which document type, that is, the fields, the editor should show, we use .schemaType('siteSettings').
  • Lastely, .documentId'(siteSettings') lets you define what the _id for the document that's created when it's edited.

Protip

If you want to limit the create and delete actions for this single document, you can do so with the action affordances feature.

There's now two “Settings” items in the first pane. This is because the document type “siteSettings” is included in the S.documentTypeListItems() as well. So what we want to do next is to filter that out from this list. Since this method returns an array, we can do that with Array.prototype.filter().

import S from '@sanity/desk-tool/structure-builder'

export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Settings')
        .child(
          S.editor()
            .schemaType('siteSettings')
            .documentId('siteSettings')
        ),
...S.documentTypeListItems().filter(listItem => !['siteSettings'].includes(listItem.getId()))
])
  • In .filter we add an anonymous function (a.k.a arrow function) that has each listItem as a parameter.
  • To get the document type name we use listItem.getId().
  • Because we might want to filter out more than this one document type, we put the document type name in an array (['siteSettings']) and use .includes() to check if the current listItem is in that array. Since we want to return false to the filter whenever a listItem is included in this array, we prepend it with the not operator (!).

Protip

If you want to get a better sense of what's going on, you can log out the listItem in the filter like this:

.filter(listItem => console.log(listItem.getId()) || !['siteSettings'].includes(listItem.getId())).

Since console.log() returns null the or operator (||) will always return the right hand expression.

The final result

Manually group document types

We have now learned how to override the default structure and how to make a new list with list items in it. Let's say we wanted to group the Category and the Project document types under a list item called "Portfolio" in the first pane, so that you end up with Settings, Portfolio, and Persons. The following code snippet may seem elaborate, but if you break it down, it should be possible to follow what happens:

import S from '@sanity/desk-tool/structure-builder'

export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Settings')
        .child(
          S.editor()
            .schemaType('siteSettings')
            .documentId('siteSettings')
        ),
// Make a new list item
S.listItem()
// Give it a title
.title('Portfolio')
.child(
// Make a list in the second pane called Portfolio
S.list()
.title('Portfolio')
.items([
// Add the first list item
S.listItem()
.title('Projects')
// This automatically gives it properties from the project type
.schemaType('sampleProject')
// When you open this list item, list out the documents
// of the type “project"
.child(S.documentTypeList('sampleProject').title('Projects')),
// Add a second list item
S.listItem()
.title('Categories')
.schemaType('category')
// When you open this list item, list out the documents
// of the type category"
.child(S.documentTypeList('category').title('Categories'))
])
),
S.listItem() .title('Persons') .schemaType('person') .child(S.documentTypeList('person').title('Persons')), ...S.documentTypeListItems().filter( listItem => !['siteSettings', 'sampleProject', 'category', 'person'].includes( listItem.getId() ) ) ])
  • First we define a new listItem in the first pane and give it the title "Portfolio".
  • In its child() method, we put a S.list() and give it a title() and items().
  • In the items array we add two list items, one for the project type, and one for the category type.
  • For these list items child we pass in the S.documenTypeList('<document type name>'), so that when each of these items are pushed, it will open a new pane with its documents, that also has the pane menu and other things automatically set up.
  • We use the .title() methods to override the default title and make it plural.
  • We use the same pattern for the person documents, only on the outmost array, which adds it to the first pane.
  • Finally, we add "project", "category", and "person" to the filter array, to remove the duplicated list items in the studio.

Make dynamic document list with GROQ filters

In many cases you want to group documents based on some field value or other properties. In the portfolio studio we can add references to Categories in the sampleProject type. Let's say that we wanted to group projects by the categories they were added to. Then we first need to make a listitem for “Projects by catgory”, then list out the different categories, and make the child of those the list of projects that belongs to each category. Let's take this step by step.

import S from '@sanity/desk-tool/structure-builder'

export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Settings')
        .child(
          S.editor()
            .schemaType('siteSettings')
            .documentId('siteSettings')
        ),
      // Make a new list item
      S.listItem()
        // Give it a title
        .title('Portfolio')
        .child(
          // Make a list in the second pane called Portfolio
          S.list()
            .title('Portfolio')
            .items([
              // Add the first list item
              S.listItem()
                .title('Projects')
                // This automatically gives it properties from the project type
                .schemaType('sampleProject')
                // When you open this list item, list out the documents
                // of the type “project"
                .child(S.documentTypeList('sampleProject').title('Projects')),
              // Add a second list item
              S.listItem()
                .title('Categories')
                .schemaType('category')
                // When you open this list item, list out the documents
                // of the type category"
                .child(S.documentTypeList('category').title('Categories')),
              // Add a new parent list item
S.listItem()
.title('Projects by category')
.child(
// List out the categories
S.documentTypeList('category')
.title('Projects by category')
// When a category is selected, pass its id down to the next pane
.child(categoryId =>
// load a new document list
S.documentList()
.title('Projects')
// Use a GROQ filter to get documents.
// This filter checks for sampleProjects that has the
// categoryId in its array of references
.filter('_type == "sampleProject" && $categoryId in categories[]._ref')
.params({categoryId})
)
)
]) ), S.listItem() .title('Persons') .schemaType('person') .child(S.documentTypeList('person').title('Persons')), ...S.documentTypeListItems().filter( listItem => !['siteSettings', 'sampleProject', 'category', 'person'].includes( listItem.getId() ) ) ])
  • First we define a new listItem for the “Projects by category”
  • To define what should appear in the next pane we add the child() method
  • Inside the child() we use the conveinence method S.documentTypeList('category') to get a list of all documents with _type == "category". This method will also set up menu items etc.
  • We don't want to return the editor for categories, but the projects that refers them, so we add another child(). Instead of passing a new list the child directly, we insert an anonymous arrow function, where the selected document’s id is the parameter we have called categoryId.
  • The we add a list of documents using S.documentList(). We define the title(), and add the filter() and params() methods.
  • The filter() method takes a GROQ filter (what goes inside of the square brackets in GROQ), and the params() method let's us define what the query variable ($categoryId) should be.
  • The filter: _type == "sampleProject" && $categoryId in categories[]._ref will return all documents that has sampleProject as the _type, and where the selected category’s _id appears inside of the objects for the key _ref in the categories array of a sampleProject document.
Projects grouped by categories

Next steps…

The capabilities of Structure Builder goes beyond what we have covered in this guide. You can experiement with resolving structures asyncronously, or even with observables. You can also customize structures with icons, initial values, menu items, and use more advanced filters to get exactly what you need.