Organizing nested pages in Sanity Studio by sitemap hierarchy
Great question! This is a common challenge when building page-builder sites with Sanity. The good news is that Structure Builder is perfect for solving this exact problem. Here's how to approach it:
The Parent-Child Reference Pattern
The most effective approach is to add a parent reference field to your page schema, then use Structure Builder to create a hierarchical view. Here's how:
1. Add a Parent Field to Your Schema
First, update your page schema to include a parent reference:
{
name: 'page',
type: 'document',
fields: [
{
name: 'title',
type: 'string'
},
{
name: 'slug',
type: 'slug'
},
{
name: 'parent',
type: 'reference',
to: [{type: 'page'}],
description: 'Select a parent page to nest this page under'
},
// ... your page builder fields
]
}2. Create a Hierarchical Structure
Now configure your Structure Builder to show top-level pages and their children. Create a structure/index.ts file:
import type {StructureResolver} from 'sanity/structure'
export const structure: StructureResolver = (S) =>
S.list()
.id('root')
.title('Content')
.items([
S.listItem()
.title('Pages')
.child(
S.documentList()
.title('Top Level Pages')
.filter('_type == "page" && !defined(parent)')
.child((documentId) =>
S.document()
.documentId(documentId)
.schemaType('page')
)
),
S.divider(),
S.listItem()
.title('All Pages (Flat View)')
.child(
S.documentTypeList('page').title('All Pages')
),
])3. Add Nested Children Views
For a more sophisticated tree structure where you can see child pages directly under their parents, you can create recursive child lists using the references() filter:
export const structure: StructureResolver = (S) =>
S.list()
.id('root')
.title('Content')
.items([
S.listItem()
.title('Site Structure')
.child(
S.documentList()
.title('Top Level Pages')
.filter('_type == "page" && !defined(parent)')
.child((documentId) =>
S.list()
.title('Page Details')
.items([
// The page document itself
S.listItem()
.title('Edit Page')
.child(
S.document()
.documentId(documentId)
.schemaType('page')
),
S.divider(),
// Child pages using references filter
S.listItem()
.title('Child Pages')
.child(
S.documentList()
.title('Child Pages')
.filter('_type == "page" && parent._ref == $parentId')
.params({parentId: documentId})
.child((childId) =>
// Recursively show grandchildren
S.list()
.title('Page Details')
.items([
S.listItem()
.title('Edit Page')
.child(
S.document()
.documentId(childId)
.schemaType('page')
),
S.divider(),
S.listItem()
.title('Child Pages')
.child(
S.documentList()
.title('Child Pages')
.filter('_type == "page" && parent._ref == $parentId')
.params({parentId: childId})
)
])
)
),
])
)
),
])Then add this to your 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
],
})Alternative: Orderable Document List
If you want manual drag-and-drop ordering along with hierarchy, consider the @sanity/orderable-document-list plugin. You'd add an orderRank field to your schema and configure it in Structure Builder for sortable lists at each level.
Displaying the Full Path
To help editors see the full path (like "Services > IT Consultancy > Data Analyst"), you can customize the preview in your schema:
preview: {
select: {
title: 'title',
parentTitle: 'parent.title',
grandparentTitle: 'parent.parent.title'
},
prepare({title, parentTitle, grandparentTitle}) {
const path = [grandparentTitle, parentTitle, title]
.filter(Boolean)
.join(' > ')
return {
title: title,
subtitle: path
}
}
}Building the Full Slug Path in Next.js
On your Next.js side, you'll want to query for the full path when fetching pages:
const query = `*[_type == "page" && slug.current == $slug][0]{
title,
slug,
"fullPath": select(
defined(parent) => parent->slug.current + "/" + slug.current,
slug.current
),
// ... other fields
}`The Structure Builder approach gives you complete control over how your pages are organized in the Studio while keeping your content model flexible. With 30+ pages, the hierarchical filtered view will make navigation much cleaner! The key is using the filter() method with parent._ref == $parentId to show only relevant child pages under each parent.
Show original thread9 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.