Typical use cases for Structure builder

An overview of typical use cases for structure builder, with code examples.

You can use Structure builder to customize how documents are grouped and listed in the Sanity Studio. In this article, you find some common, but minimal use cases with complete code examples. You may want to work through the introduction guide, or the overview article first. For more in-depth documentation of the methods in Structure builder, check out the reference documentation.

Singletons and "one-off" documents

Use Structure Builder to list document types with just one document.

Often you want to restrict a document type to having just one document. This is typically some sort of global configuration, or information that's singular, such as your company’s contact information. When you add a document type, the Studio will per default allow people to create multiple documents of that type. You can use the Structure builder API to limit a document type to a limited set of document(s) with predefined id(s).

This approach is also future proofing your content model, because if the time comes where you need to have multiple configurations, or you need add contact information for your new branch, you can always “break out” of the singleton by reconfiguring the structure.

Let's look at an example:

// deskStructure.js
import S from "@sanity/desk-tool/structure-builder";
 
export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Site settings')
        .child(
          S.document()
            .schemaType('siteSettings')
            .documentId('siteSettings')
        ),
      // Add a visual divider (optional)
      S.divider(),
      // List out the rest of the document types, but filter out the config type
      ...S.documentTypeListItems()
        .filter(listItem => !['siteSettings'].includes(listItem.getId()))
    ])

This definition will produce the following document structure:

The portfolio studio from sanity.io/create

Gotcha

Remember to create the initial singleton document before overriding the default publishing actions in the next section.

Override the default publishing actions

When you create “one of” or “some of” documents, you most often also want to restrict what publishing actions can be done with those documents. Typically you only want to allow for updating and publishing new document changes. For now, use the experimental action UI affordances in the schema definition to define which actions should be allowed:

// /schemas/documents/siteSettings.js

export default {
  name: 'siteSettings',
  type: 'document',
  title: 'Site settings',
__experimental_actions: [/*'create',*/ 'update', /*'delete',*/ 'publish'],
fields: [ /* the fields */ ] }

This will remove siteSettings from the “Create new…” menus, and remove the delete button from the action bar.

The additional actions are disabled for the site settings document type.

Manual grouping of documents

When a project matures, the content model tends to grow with many document types. At some point you probably want to organize these document types differently than listing them out in the root pane. With the Structure builder API you can override the default lists and add your own list items and define what goes into them.

Adding additional site settings documents

Building on the previous example, let's say you create another website, and want to use the same project and dataset to manage content there as well. You can add another settings document in your structure definition by adding an S.listItem in the top list, and in its child, add a new S.list that contains the “siteSettings” item, as well as your new “anotherSiteSettings” item.

// deskStructure.js
import S from "@sanity/desk-tool/structure-builder";
 
export default () =>
  S.list()
    .title('Content')
.items([
S.listItem()
.title('Configs')
.child(
S.list()
.title('Configs')
.items([
S.document()
.schemaType('siteSettings')
.documentId('siteSettings'),
S.document()
.schemaType('siteSettings')
.documentId('anotherSiteSettings'),
])
), // Add a visual divider (optional) S.divider(), // List out the rest of the document types, but filter out the config type ...S.documentTypeListItems() .filter(listItem => !['siteSettings'].includes(listItem.getId())) ])

Dynamic grouping of documents

It's often useful to group documents automatically by some field value, or a combination of field values. Common examples are grouping documents by author, publishing date periods, editorial status, by category, or even by the dominant background color in a document’s main image.

This example shows how you can list documents by the category documents they reference.

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

export default () =>
  S.list()
    .title('Content')
    .items([
      // List out all the document types in schema.js
      ...S.documentTypeListItems(),
      // Add a new list item for projects by category
      S.listItem()
        .title('Projects by category')
        .child(
          // List out all categories
          S.documentTypeList('category')
            .title('Projects by category')
            .child(catId =>
              // List out project documents where the _id for the selected
              // category appear as a _ref in the project’s categories array
              S.documentList()
                .schemaType('sampleProject')
                .title('Projects')
                .filter(
                  '_type == "sampleProject" && $catId in categories[]._ref'
                )
                .params({ catId })
            )
        ),
    ])

The rationale here is that we first list out the available category documents, and then use their id (called catId in the configuration) in a GROQ filter to generate a list of sampleProject documents.

Gotcha

The .filter() function (that is, method) used in this example, is not the Array.prototype.filter function that you may be used to from JavaScript elsewhere. It is method connected to S.documentList() that returns documents from a GROQ-filter.

Tabs with content previews

Read the blog post about views and split panes →

Fill tabs with any content you'd like, and use split views to see them side by side.

The Structure Builder API also gives you control over how a document node is presented within a collapsible pane. More specifically, it lets you set up one to multiple Views that either return the default form, or a React component. A View receives a collection of props, including the values for the document in different states: draft, published, historical, and currently displayed (e.g. if you have selected a specific document revision).

Structure Builder comes with a getDefaultDocumentNode function for defining the default tabs for document views, which you define in deskStructure.js. In this example we use it to make a simple JSON preview for all document types where another view hasn't been set in a structure definition.

// deskStructure.js
import React from 'react'
import S from '@sanity/desk-tool/structure-builder'

const JsonPreview = ({document}) => (
  // The JSON preview
  <pre>{JSON.stringify(document.displayed, null, 2)}</pre>
)

export const getDefaultDocumentNode = () => 
  // Give all documents the JSON preview, 
  // as well as the default form view
  S.document().views([
    S.view.form(),
    S.view.component(JsonPreview).title('JSON')
  ])

export default S.defaults()

To define custom Views for specific document types only, you can use the schemaType prop inside an if statement:

// deskStructure.js
import React from 'react'
import S from '@sanity/desk-tool/structure-builder'

const JsonPreview = ({document}) => (
  // The JSON preview
  <pre>{JSON.stringify(document.displayed, null, 2)}</pre>
)

export const getDefaultDocumentNode = ({schemaType}) => {
  if(schemaType == 'myDocument') {
    // Give all documents of type myDocument the JSON preview, 
    // as well as the default form view
    S.document().views([
      S.view.form(),
      S.view.component(JsonPreview).title('JSON')
    ])
  }
}

export default S.defaults()

You can also define views for documents by their ID with the documentId prop:

// deskStructure.js
import React from 'react'
import S from '@sanity/desk-tool/structure-builder'

const JsonPreview = ({document}) => (
  // The JSON preview
  <pre>{JSON.stringify(document.displayed, null, 2)}</pre>
)

export const getDefaultDocumentNode = ({documentId}) => {
  if(documentId == 'siteSettings') {
    // Only give the document with ID siteSettings the JSON preview, 
    // as well as the default form view
    S.document().views([
      S.view.form(),
      S.view.component(JsonPreview).title('JSON')
    ])
  }
}

export default S.defaults()

Read more about the getDefaultDocumentNode function in the Structure Builder reference.

Defining a structure asynchronously with promises or observables

The structure builder supports async resolving of its members either through promises or observables. This means you could be making structures based on values returned from an API, like another Sanity document. Take this example:

// deskStructure.js
import documentStore from 'part:@sanity/base/datastore/document'
/*
 * Remember to install rxjs or rxjs/operators in your studio:
 * $ yarn add rxjs
 */
import {map} from 'rxjs/operators'

export default S.list()
  .title('Content')
  .items([
    S.listItem()
      .title('A list depending on values from another document')
      .child(() =>
        documentStore.listenQuery(`*[_id == 'myConfigDoc'][0]`)
          .pipe(
            map(myConfigDoc => {
              return S.documentList({
                id: 'list-with-config-from-another-document',
                title: myConfigDoc.listTitle,
                options: {
                  filter: myConfigDoc.listFilter
                }
              })
            })
          )
      )
  ])

Here we set up a listener (documentStore.listenQuery) for a special document that contains the title and filter for our S.documentList. When that document changes, the list will be updated in realtime for everyone. We could also be doing this with a promise that fetches the document initially, but then the user would have to reload the studio to see the new structure based on changes in the config document that was done in the meantime.

Was this article helpful?