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

14 replies
Last updated: Feb 10, 2021
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:

  1. The child() function receives the clicked document's ID
  2. It creates a new list showing the contents of that directory
  3. Subdirectories use the same child resolver pattern, creating infinite nesting
  4. GROQ filters with parent._ref == $parentId find children of each directory

Benefits of This Approach

  • Documents stay documents - Your content items remain full documents, independently queryable and referenceable
  • Infinitely nestable - As deep as you need to go
  • Flexible organization - Documents without a parent live at the root level
  • Easy querying - You can query the hierarchy with GROQ:
*[_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
You can use filters inside the desk structure to create your own lists. Does this help? https://www.sanity.io/docs/structure-builder-typical-use-cases#dynamic-grouping-of-documents-56a7d9c846a6
for example how I have set up events in one of my projects

S.listItem()
  .title(getDocumentTitle("event"))
  .icon(getDocumentIcon("event"))
  .child(
    S.list()
      .title(getDocumentTitle("event"))
      .items([
        S.listItem()
          .title("All events")
          .icon(getDocumentIcon("event"))
          .child((id) =>
            S.documentList()
              .title("All events")
              .filter('_type == "event"')
              .menuItems([...S.documentTypeList("event").getMenuItems()])
          ),

        S.divider(),
        S.listItem()
          .title("Upcoming events")
          .icon(getDocumentIcon("event"))
          .child((id) =>
            S.documentList()
              .title(getDocumentTitle("event"))
              .filter(`_type == "event" && date > "${today}"`)
              .menuItems([...S.documentTypeList("event").getMenuItems()])
          ),
        S.listItem()
          .title("Past events")
          .icon(getDocumentIcon("event"))
          .child((id) =>
            S.documentList()
              .title(getDocumentTitle("event"))
              .filter(`_type == "event" && date < "${today}"`)
              .menuItems([...S.documentTypeList("event").getMenuItems()])
          ),
      ])
  ),
This isn't exactly what I was looking for, but could possibly be of help. What I ideally want is for our content editors to be able to edit the desk structure and create arbitrary hierarchies of documents.
well if you make a schema for the desk structure you could use that for filtering right?
Possibly? I don't see how 🙂 Let's say I have article and directory. I want the top-level structure to contain a mix of article and directories. Each directory should in turn open a new desk pane with more articles and directories.
I'm not sure how to create such a "directory" type.
I'm also not sure how to create recursive desk structure of unknown depth this way.
ah the recursiveness is an issue yeah. Maybe you could do a query at the start of the deskStructure using the js client and use that to create a loop of listItems with children? Not sure that would work though.
Seems to be an example of the same kind of filtering you linked to above?
user Y
(if you have the time) would that gist work for recursive structures?
Soooo. It's technically possible to create a “desk structure configuration document” where you create a nested array input that at least goes 20 level deep (that's the technical level for how far we index I believe). You can use this structure to generate the structure since it supports both promises and observables.
I'm not sure if I have time for a proof of concept this week, I'm afraid
No worries! Would you say that doing this is not really recommended? Sounds a little "off the beaten path"
(Sorry for the extreme delay, got caught up elsewhere and forgot about this 😢 )

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?