GROQ-Powered Webhooks – Intro to Projections
A thorough intro to using GROQ-projections in a webhook contest
Go to GROQ-Powered Webhooks – Intro to ProjectionsStructure builder lets you override the default document lists in the Studio, and gives full flexibility to how documents are grouped.
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
(see highlighted code), 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"
],
"parts": [
{
"name": "part:@sanity/base/schema",
"path": "./schemas/schema.js"
},
{
"name": "part:@sanity/desk-tool/structure",
"path": "./deskStructure.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.
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.document()
.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.S.listItem().title('Settings')
which creates the top item in the first pane..child()
..child()
we put the S.component()
, which tells the Studio that when you click on the item with the title "Settings", it should return a document editor in the next pane..schemaType('siteSettings')
..documentId'(siteSettings')
lets you define what the _id
for the document that's created when it's edited.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.document()
.schemaType('siteSettings')
.documentId('siteSettings')
),
...S.documentTypeListItems().filter(listItem => !['siteSettings'].includes(listItem.getId()))
])
.filter
we add an anonymous function (a.k.a arrow function) that has each listItem
as a parameter. listItem.getId()
. ['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 (!
).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.
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.document()
.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()
)
)
])
listItem
in the first pane and give it the title "Portfolio".child()
method, we put a S.list()
and give it a title()
and 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..title()
methods to override the default title and make it plural.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 list item for “Projects by category”, 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.document()
.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()
)
)
])
listItem
for the “Projects by category”child()
methodS.documentTypeList('category')
to get a list of all documents with _type == "category"
. This method will also set up menu items etc.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
.S.documentList()
. We define the title()
, and add the filter()
and params()
methods.$categoryId
) should be._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.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.
A thorough intro to using GROQ-projections in a webhook contest
Go to GROQ-Powered Webhooks – Intro to ProjectionsA thorough intro to using GROQ-filters in a webhook-context
Go to GROQ-Powered Webhooks – Intro to FiltersA step-by-step guide to setup Next.js and Sanity Studio with Live Preview
Go to Live Preview with Next.js and Sanity.io: A Complete GuideHow to deliver better content experiences across cultures, languages, and tastes.
Go to Tailoring content for different audiences