Organizing nested pages in Sanity Studio by sitemap hierarchy

9 replies
Last updated: Nov 24, 2021
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
user K
I'd be curious about the hierarchy aspect of the project. We can definitely throw a couple of tips your way as we currently have a page builder built over here: https://www.userfylabs.co.uk/ in what I would consider quite a scalable way
Perfect, thanks for the assistance so far! 😊
user C
As per now, all pages are being generated from the same schema document (page.js). For example, using the same page-builder schema template I want to be able to create a page for ".com/services/subpage/nested-subpage" and for ".com/services". I have not applied any filtering to deskStructure.js so all these page documents (from page.js schema model) are listed inside the same view, regardless of what is specified inside the slug field. But I will experiment with this, as proposed by
user A
user C
Can I ask - how do you define parent pages of a new page in your system? Would you recommend using an array of "references" to other page documents in order to create the path?
Hi
user K
we're actually in the process of figuring out the best way of doing this ourselves. We're using this tutorial here (https://www.sanity.io/guides/hierarchies-graphs-navigation#7a89a57242da ) to form our baseline. E.g dependent on how rigid your .com/services/subpage/nested-subpage is, this might work, however if you were going to create .com/*dynamic*/subpage/nested-subpage it becomes harder.
Something else that's going to help you for understanding current path is this:
https://www.sanity.io/plugins/better-slug
Yes, .com/*dynamic*/subpage/nested-subpage would be representative for what I want to achieve. Nice..! That slug extension looks quite useful indeed
Yes, .com/*dynamic*/subpage/nested-subpage would be representative for what I want to achieve. Nice..! That slug extension looks quite useful indeed
user C
Thank you very much for that gist. Looks like this achieves exactly what I was looking to do 🙏
Hey Mikkel, didn't see this come through. Glad you're getting some progress. If it's any help that slug extension is pretty good but there's some issues with the length of the slug cutting off. That being said, this might help too: https://www.sanity.io/schemas/easy-peasy-url-slug-d2400b42
I've just popped it up earlier today because I was working on it this morning and it was driving me nuts.

Good luck on the project, and please let me know how you get on
🎉

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?