Organizing nested pages in Sanity Studio by sitemap hierarchy

9 replies
Last updated: Nov 30, 2025
Hey there! 🙂 I am using Sanity with NextJS and my goal is to configure Sanity Studio with page-builder functionality. My only challenge in doing this is that I have not yet found a logical/meaningful way of organising the documents created with my custom page-builder schema type. All my dynamic page documents are listed in the same document list regardless of where they belong on the sitemap. For example: pages that are subpages of subpages (like ".com/services/it-consultancy/data-analyst") end up in the same list view as top level pages (like ".com/services" or ".com/about"). This gets messy quickly with 30+ pages to be created.
How can I configure or customise Sanity Studio pane/list behaviour to sort and nest pages based on where they belong on the sitemap? Or do I need to think differently about this - is there an alternative approach that's better? Thank you very much in advance!
😊
AI Update

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 thread
9 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.

Was this answer helpful?