How to build a Remix website with Sanity.io and live preview - has 21 likes
Combine Sanity's blazing-fast CDN with Remix's cached at the edge pages.
Go to How to build a Remix website with Sanity.io and live previewCreating Parent / Child relationships in Sanity goes beyond a parent
reference field. In this guide we'll include initial value templates, filtered document lists and guides on how to effectively use and query documents that use these taxonomy schema.
Here's what we'll be building.
category
schema type with Parent documentsIf you're looking for an even fancier drag-and-drop Hierarchical Document List, this plugin has you covered!
First, we'll need a schema for our taxonomy. We'll call ours category
for this guide. You may call yours tag
, section
, or something else.
This guide will focus on building a simple, two-tier, parent/child hierarchy. But the ideas here could be extended further to deeper relationships.
category
document that does not have the parent
field defined.category
document that with a parent field reference.Add the below to your studio files, and register it to your Studio's schema.
// ./src/schema/category.js
import {FiTag} from 'react-icons/fi'
export default {
name: 'category',
title: 'Category',
type: 'document',
icon: FiTag,
fields: [
{name: 'title', type: 'string'},
{
name: 'parent',
type: 'reference',
to: [{type: 'category'}],
// This ensures we cannot select other "children"
options: {
filter: '!defined(parent)',
},
},
],
// Customise the preview so parents are visualised in the studio
preview: {
select: {
title: 'title',
subtitle: 'parent.title',
},
prepare: ({title, subtitle}) => ({
title,
subtitle: subtitle ? `– ${subtitle}` : ``,
}),
},
}
Before setting up the Desk Structure, make sure you have Initial Value Templates configured in the Studio.
With the right configuration, we can create Document Lists which show all Children of a specific Parent, and when creating a new document from that list pre-fill the parent
reference field with that same Parent!
Here's we create a template called "Category: Child". Make sure this file is loaded from the parts in sanity.json
also.
// ./src/initial-value-templates/index.js
import T from '@sanity/base/initial-value-template-builder'
export default [
T.template({
id: 'category-child',
title: 'Category: Child',
schemaType: 'category',
parameters: [{name: `parentId`, title: `Parent ID`, type: `string`}],
// This value will be passed-in from desk structure
value: ({parentId}) => ({
parent: {_type: 'reference', _ref: parentId},
}),
}),
// Insert all your other Templates
...T.defaults(),
]
Desk Structure is a complex part of the Studio. The code we'll use here is no exception.
Below is a helper function to create structured parent/child lists of documents.
This could be enqueued into your Structure builder along with any other items like this:
// ./src/desk-structure/index.js
import parentChild from './parentChild'
export default () => S.list()
.title('Content')
.items([
S.documentTypeListItem('product').title('Products'),
parentChild('category'),
S.divider(),
S.documentTypeListItem('page').title('Pages'),
])
The parentChild()
helper function only accepts one parameter for now – the schema type – but you could extend it further for reuse by including parameters for Titles, Icons, etc.
This desk structure item is more dynamic than most. It will query the documentStore
for all parent categories and create a S.listItem()
for each one. Inside those, it will show all category documents with that parent as a reference.
// ./src/desk-structure/parent-child.js
import S from '@sanity/desk-tool/structure-builder'
import documentStore from 'part:@sanity/base/datastore/document'
import {map} from 'rxjs/operators'
import {FiTag} from 'react-icons/fi'
// You may need to customise your `views` array here for adding live preview iframes, incoming references, etc
const views = [S.view.form()]
export default function parentChild(schemaType = 'category') {
const categoryParents = `_type == "${schemaType}" && !defined(parent) && !(_id in path("drafts.**"))`
return S.listItem(schemaType)
.title('Categories')
.icon(FiTag)
.child(() =>
documentStore.listenQuery(`*[${categoryParents}]`).pipe(
map((parents) =>
S.list()
.title('All Categories')
.menuItems([
S.menuItem()
.title('Add Category')
.icon(FiTag)
.intent({type: 'create', params: {type: schemaType}}),
])
.items([
S.listItem()
.title('Parent Categories')
.schemaType(schemaType)
.child(() =>
S.documentList()
.schemaType(schemaType)
.title('Parent Categories')
.filter(categoryParents)
.canHandleIntent(() => S.documentTypeList(schemaType).getCanHandleIntent())
.child((id) => S.document().documentId(id).views(views).schemaType(schemaType))
),
S.divider(),
...parents.map((parent) =>
S.listItem({
id: parent._id,
title: parent.title,
schemaType,
child: () =>
S.documentTypeList(schemaType)
.title('Child Categories')
.filter(`_type == "${schemaType}" && parent._ref == $parentId`)
.params({parentId: parent._id})
.initialValueTemplates([
S.initialValueTemplateItem('category-child', {
parentId: parent._id,
}),
]),
})
),
])
)
)
)
}
Note that accessing the documentStore
directly like this is not common and on a larger dataset may produce undesirable results.
parent
reference should be pre-filled.Consider when using these taxonomies to restrict references to Children.
For example in a schema of post, instead of an array of references where the author may add Parent and Child category references – have them select only "Child" documents.
// ./src/schema/post.js
import {FiFileText} from 'react-icons/fi'
export default {
name: 'post',
title: 'Post',
type: 'document',
icon: FiFileText,
fields: [
{
name: 'category',
type: 'reference',
to: [{type: 'category'}],
options: {filter: 'defined(parent)'},
},
// ...other fields
],
}
Then when querying for a post, "follow" the Child category up to retrieve its parent.
*[_type == "post"]{
category->{
parent->
}
}
Each category
document has a slug, but in a hierarchal website structure you may wish for Children to be nested inside Parents.
With some a clever GROQ function, we can do that from inside our query.
Here's a basic query for all category
titles and slugs:
*[_type == "category"]{
title,
"parentSlug": parent->slug.current,
"slug": slug.current
}
The response will look something like this. Which has the right data, but requires us to post-process the results to build the slug we need.
[
{
title: "Liquorice",
slug: "liquorice"
},
{
title: "Dutch",
parentSlug: "liquorice",
slug: "dutch"
}
]
Instead, using the select
function in GROQ allows us to return a different value depending on a condition. In this case, whether a category has a parent field or not.
select
works by returning whichever condition returns true first, and resolves the last item if nothing returns true.
The first condition defined(parent)
will be true for any Child category. Otherwise, the fallback is the document's own slug.
*[_type == "category"]{
title,
"slug": select(
defined(parent) => parent->slug.current + "/" + slug.current,
slug.current
)
}
This would now instead return data that looks like this:
[
{
title: "Liquorice"
slug: "liquorice"
},
{
title: "Dutch"
slug: "liquorice/dutch"
}
]
Hierarchical document schema like categories express the power of structured content, strong references and GROQ queries.
Your authors should now be able to create and use these taxonomical documents throughout your content with confidence!
Combine Sanity's blazing-fast CDN with Remix's cached at the edge pages.
Go to How to build a Remix website with Sanity.io and live previewSometimes the content you need to reference lives outside of Sanity
Go to Creating a custom input to display and save third party dataA 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 GuideGet the best of both worlds. Tailwind-styled typography and Portable Text's markup-and-components structure.
Go to ProseableText: Combine Tailwind CSS Typography with Portable Text