Joint session with Vercel: How to build intelligent storefronts (May 15th)

Building a directory tree structure for document organization in Sanity desk tool

14 repliesLast updated: Dec 1, 2025

As our content grows we're getting som feedback that some listings in the desk tool are long and hard to navigate. Is it possible to build some sort of "directory tree" of content? E.g. in a listing, allow documents of type A, OR "directories" that can contain documents of type A or directories and so on. I want the documents to be documents, not objects, do I don't want to create "directory" type with a list of objects in it.
AI Update

Yes, you can definitely build a directory tree structure in Sanity! The key is using a parent reference field on both your directory documents and content documents, then leveraging the Structure Builder's child resolvers to create the navigation hierarchy.

Schema Setup

First, create a directory document type with a parent reference to itself:

// schemas/directory.ts
export default {
  name: 'directory',
  type: 'document',
  title: 'Directory',
  fields: [
    {
      name: 'title',
      type: 'string',
      title: 'Directory Name'
    },
    {
      name: 'parent',
      type: 'reference',
      title: 'Parent Directory',
      to: [{type: 'directory'}],
      description: 'Leave empty for root-level directories'
    }
  ]
}

Then add a parent reference to your content documents:

// schemas/article.ts (or whatever your type A is)
export default {
  name: 'article',
  type: 'document',
  fields: [
    // ... your existing fields
    {
      name: 'parent',
      type: 'reference',
      title: 'Parent Directory',
      to: [{type: 'directory'}],
      description: 'Organize this document in a directory'
    }
  ]
}

Structure Builder Implementation

Here's how to build the tree view using child resolvers. The important thing to understand is that child resolvers respond to user navigation - when someone clicks on a directory, the child() function determines what shows in the next pane:

// structure/index.ts
import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('📁 Content Tree')
        .child(
          S.list()
            .title('Root')
            .items([
              // Root-level directories
              S.listItem()
                .title('Directories')
                .child(
                  S.documentList()
                    .title('Root Directories')
                    .filter('_type == "directory" && !defined(parent)')
                    .child((documentId) =>
                      // This child resolver is called when a directory is clicked
                      S.list()
                        .title('Contents')
                        .items([
                          // Subdirectories in this directory
                          S.listItem()
                            .title('📁 Subdirectories')
                            .child(
                              S.documentList()
                                .title('Subdirectories')
                                .filter(`_type == "directory" && parent._ref == $parentId`)
                                .params({parentId: documentId})
                                .child((childDocId) =>
                                  // Recursive navigation - same structure for subdirectories
                                  S.list()
                                    .title('Contents')
                                    .items([
                                      S.listItem()
                                        .title('📁 Subdirectories')
                                        .child(
                                          S.documentList()
                                            .title('Subdirectories')
                                            .filter(`_type == "directory" && parent._ref == $parentId`)
                                            .params({parentId: childDocId})
                                            // You can nest this pattern as deep as needed
                                        ),
                                      S.listItem()
                                        .title('📄 Documents')
                                        .child(
                                          S.documentList()
                                            .title('Documents')
                                            .filter(`_type == "article" && parent._ref == $parentId`)
                                            .params({parentId: childDocId})
                                        ),
                                      S.divider(),
                                      S.documentListItem()
                                        .id(childDocId)
                                        .schemaType('directory')
                                        .title('Edit this directory')
                                    ])
                                )
                            ),
                          // Documents in this directory
                          S.listItem()
                            .title('📄 Documents')
                            .child(
                              S.documentList()
                                .title('Documents')
                                .filter(`_type == "article" && parent._ref == $parentId`)
                                .params({parentId: documentId})
                            ),
                          S.divider(),
                          // Allow editing the directory itself
                          S.documentListItem()
                            .id(documentId)
                            .schemaType('directory')
                            .title('Edit this directory')
                        ])
                    )
                ),
              // Root-level documents (no parent)
              S.listItem()
                .title('📄 Documents')
                .child(
                  S.documentList()
                    .title('Root Documents')
                    .filter('_type == "article" && !defined(parent)')
                )
            ])
        ),
      S.divider(),
      // Keep flat views as backup for finding content
      S.documentTypeListItem('directory').title('All Directories'),
      S.documentTypeListItem('article').title('All Articles'),
    ])

How This Works

The Structure Builder's child resolver pattern works by responding to user navigation. When someone clicks on a directory in the list:

Benefits of This Approach

*[_type == "article" && _id == $id][0] {
  _id,
  title,
  parent->{
    title,
    parent->{
      title,
      parent->{title}
    }
  }
}

Practical Note

The example above shows nesting 2-3 levels deep explicitly. For deeper nesting, you'd want to extract the list-building logic into a reusable function that you can call at each level, but the pattern remains the same: use child() with the documentId to create filtered lists of subdirectories and documents.

This gives your editors a familiar folder-like browsing experience while maintaining Sanity's flexible document-based architecture!

Show original thread
14 replies

Was this answer helpful?

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.

Related contributions