Tree View

Representing site content within a controlled, hierarchical view in the content editor.

After configuring a content source, Stackbit automatically lists models in the content editor, separated by model type (only page and data models are shown).

A tree view can be added using the treeViews property, which provides the ability to create a customized representation of the site content.

Tree Node Types

Nodes can be one of two types:

  • Text Node: A label that groups multiple documents together.
  • Document Node: A node that represents a document, which can be edited from the document list on the right side of the screen.

See the reference for how the objects differ between the node types.

Building a Tree

Building a tree typically involves fetching and filtering documents, then building a recursive list of TreeViewNode objects.

Displaying Children

As nodes in the tree are highlighted, their children are displayed in the panel to the right of the tree, providing the option to edit document nodes.

Note that all root nodes must have children, otherwise the tree will not be rendered.

Editing Documents

Document nodes can be edited by clicking the Edit button when they appear in the panel on the right (when their parents is the active tree item).

Fetching Documents

The typical pattern for building a tree begins with fetching content from the site. This is usually done via the getDocuments() method provided to the treeViews within the options parameter.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
export default defineStackbitConfig({
  treeViews: async (options) => {
    const allPages = options
      .getDocuments()
      .filter((document) => document.modelName === 'Page')
    //
    // Build the tree ...
    //
  },
})

Examples

Here are a few very simple examples to get started. Both were built using Git CMS, but can be applied to any content source.

List of Page Documents

This example creates a root node called "Site Pages" and lists all document of model Page under it.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
import {
  defineStackbitConfig,
  DocumentWithSource,
  TreeViewNode,
} from '@stackbit/types'

export default defineStackbitConfig({
  stackbitVersion: '~0.6.0',
  contentSources: [
    /* ... */
  ],
  treeViews: async ({ getDocuments }) => {
    const children: TreeViewNode['children'] = getPages(getDocuments()).map(
      (document) => ({
        document,
        label: getFieldValue(document, 'title'),
      }),
    )
    return [
      { label: 'Site Pages', children, stableId: 'pages-tree' },
    ] as TreeViewNode[]
  },
})

function getFieldValue(page: DocumentWithSource, field: string) {
  const fieldObject = page.fields[field]
  if (!fieldObject || !('value' in fieldObject)) return
  return fieldObject.value
}

function getPages(documents: DocumentWithSource[]) {
  return documents.filter((document) => document.modelName === 'Page')
}

Listing Pages by URL Path

Here's a more complex example, which builds a tree based on a nested URL structure, similar to how the sitemap navigator behaves.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
import {
  defineStackbitConfig,
  DocumentWithSource,
  TreeViewNode,
} from '@stackbit/types'

export default defineStackbitConfig({
  stackbitVersion: '~0.6.0',
  ssgName: 'nextjs',
  nodeVersion: '16',
  contentSources: [
    /* ... */
  ],
  treeViews: async ({ getDocuments }) => {
    type UrlTree = {
      [key: string]: UrlTreeNode
    }

    type UrlTreeNode = {
      id: string
      document?: DocumentWithSource
      slug?: string
      children?: UrlTree
    }

    type ReducedUrlTree = {
      tree: UrlTree
      urlPath?: string
    }

    let urlTree: UrlTree = {}

    getPages(getDocuments()).forEach((page) => {
      const urlParts = getUrlParts(page)
      let docNode
      urlParts.reduce<ReducedUrlTree>(
        (acc, part): ReducedUrlTree => {
          const id = acc.urlPath ? `${acc.urlPath}__${part}` : part
          if (!acc.tree[part]) acc.tree[part] = { id }
          if (!acc.tree[part].children) acc.tree[part].children = {}
          docNode = acc.tree[part]
          return { tree: acc.tree[part].children || {}, urlPath: id }
        },
        { tree: urlTree },
      )
      docNode.document = page
      docNode.slug = urlParts[urlParts.length - 1]
    })

    function pagesTree(tree?: UrlTree): TreeViewNode[] {
      if (!tree || Object.keys(tree).length === 0) return []
      return Object.entries(tree)
        .map(([slug, node]) => {
          const children = pagesTree(node.children)
          const label = slug === '/' ? 'Home Page' : `/${slug}`
          if (node.document) {
            return { document: node.document, children, label }
          }
          return { label, children, stableId: node.id }
        })
        .filter(Boolean) as TreeViewNode[]
    }

    const tree: TreeViewNode[] = [
      {
        label: 'Site Pages',
        children: pagesTree(urlTree),
        stableId: 'pages-tree',
      },
    ]

    return tree
  },
})

function getFieldValue(page: DocumentWithSource, field: string) {
  const fieldObject = page.fields[field]
  if (!fieldObject || !('value' in fieldObject)) return
  return fieldObject.value
}

function getPages(documents: DocumentWithSource[]) {
  return documents.filter((document) => document.modelName === 'Page')
}

function getUrlParts(page: DocumentWithSource) {
  const urlParts = `/${getFieldValue(page, '_filePath_slug')}`
    .replace(/^[\/]+/, '/')
    .replace(/\/index$/, '')
    .split('/')
    .filter(Boolean)
  if (urlParts.length === 0) urlParts.push('/')
  return urlParts
}