Visual editing /Visual Editor /

Tree view

A tree view represents the site content within a controlled, hierarchical view in the content editor.

After configuring a content source, Visual Editor 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.

# Build a tree

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

# Display 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.

# Edit 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).

# Fetch 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.

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.

//stackbit.config.ts
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");
}

# List 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.

//stackbit.config.ts
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;
}