Structuring Nested Categories and Tags in Sanity Desk
I totally get the frustration - this is a really common pattern people need! The key issue you're running into is that filtered lists in Structure Builder are read-only views. You can't create new documents directly in them because Sanity doesn't know what filter values to pre-populate.
Here's the best approach that gives you both organized browsing AND the ability to create documents:
The Solution: Separate Document Types with References
Create two document types where tags reference their parent category:
// schemas/category.ts
export default {
name: 'category',
type: 'document',
title: 'Category',
fields: [
{
name: 'title',
type: 'string',
title: 'Title',
validation: (Rule) => Rule.required()
},
{
name: 'slug',
type: 'slug',
title: 'Slug',
options: { source: 'title' }
}
]
}
// schemas/tag.ts
export default {
name: 'tag',
type: 'document',
title: 'Tag',
fields: [
{
name: 'title',
type: 'string',
title: 'Title',
validation: (Rule) => Rule.required()
},
{
name: 'category',
type: 'reference',
title: 'Category',
to: [{type: 'category'}],
validation: (Rule) => Rule.required()
}
]
}Structure Builder Setup
Now create a Structure Builder configuration that gives you both the hierarchical view and flat creation lists:
// structure/index.ts
import type {StructureResolver} from 'sanity/structure'
export const structure: StructureResolver = (S) =>
S.list()
.title('Content')
.items([
// Hierarchical browsing view
S.listItem()
.title('Tags by Category')
.child(
S.documentTypeList('category')
.title('Categories')
.child((categoryId) =>
S.documentList()
.title('Tags')
.filter('_type == "tag" && category._ref == $categoryId')
.params({ categoryId })
)
),
S.divider(),
// Flat lists for creating new documents
S.documentTypeListItem('category').title('All Categories'),
S.documentTypeListItem('tag').title('All Tags'),
])Wire it up in your config:
// sanity.config.ts
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {structure} from './structure'
export default defineConfig({
// ...other config
plugins: [
structureTool({structure}),
// ...other plugins
],
// ...schemas
})How to Use This Setup
To create a new category:
- Go to "All Categories" in the sidebar
- Click the "+" button
- Fill in the category details
To create a new tag:
- Go to "All Tags" in the sidebar
- Click the "+" button
- Fill in the tag title and select its category from the reference field
To browse tags by category:
- Go to "Tags by Category"
- Click on a category
- See all tags filtered to that category
Why This Works
The "Tags by Category" view uses child resolvers to create a hierarchical browsing experience - perfect for viewing and editing existing content in an organized way. The "All Categories" and "All Tags" lists give you unfiltered access where Sanity can properly handle document creation without needing to guess filter values.
This pattern is exactly how parent-child taxonomies are meant to work in Sanity. You get the best of both worlds: intuitive organization for browsing/editing and straightforward workflows for creating new content! 😊
Show original thread5 replies
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.