Recursive query for page-tree plugin navigation with dynamic depth

2 replies
Last updated: Apr 4, 2024
Hello. Working on a documentation site using this plugin: https://www.sanity.io/plugins/page-tree . What would a query for a navigation sidebar be? Have gotten it to work, but using reference means i have to hardcode the max depth of the tree, so i feel like a better solution would maybe be recursive. Any ideas?
*[_type == 'homePage']{
    title,
    'contentpages': *[_type == 'contentPage' && references(^._id)]{
      title
    }
  }
AI Update

I understand your challenge! GROQ doesn't have built-in recursive querying capabilities, so you're right that using the reference operator (->) requires hardcoding the depth. However, there are a few approaches you can take to handle hierarchical navigation with the page-tree plugin.

The Core Challenge

GROQ processes queries in a single pass and doesn't support recursive traversal. So when you write contentpages->children->children, you have to manually specify each level. This is a known limitation when working with tree structures.

Better Approaches

1. Fetch the flat structure and build the tree client-side

Instead of trying to build the nested structure in GROQ, fetch all pages with their parent references and reconstruct the tree in your application:

*[_type == 'contentPage'] {
  _id,
  title,
  slug,
  "parentId": parent._ref,
  "order": order
} | order(order asc)

Then in your JavaScript/TypeScript code, build the tree structure:

function buildTree(flatPages) {
  const map = {};
  const roots = [];
  
  flatPages.forEach(page => {
    map[page._id] = { ...page, children: [] };
  });
  
  flatPages.forEach(page => {
    if (page.parentId && map[page.parentId]) {
      map[page.parentId].children.push(map[page._id]);
    } else {
      roots.push(map[page._id]);
    }
  });
  
  return roots;
}

This gives you full flexibility and avoids the hardcoded depth limitation.

2. Use a reasonable depth with projections

If you know your navigation won't exceed a certain depth (most documentation sites have 3-4 levels max), you can create a more maintainable query using subqueries:

*[_type == 'homePage'][0] {
  title,
  'navigation': *[_type == 'contentPage' && !defined(parent)] | order(order asc) {
    _id,
    title,
    slug,
    'children': *[_type == 'contentPage' && parent._ref == ^._id] | order(order asc) {
      _id,
      title,
      slug,
      'children': *[_type == 'contentPage' && parent._ref == ^._id] | order(order asc) {
        _id,
        title,
        slug
      }
    }
  }
}

This uses subqueries with the parent scope operator (^) to reference the parent document's _id rather than dereferencing, which can be cleaner than chaining -> operators.

3. Consider Sanity Functions for dynamic generation

If you need the tree structure server-side for SEO or performance reasons, you could use Sanity Functions to build and cache the navigation tree. This would run serverless code that can use loops to build the tree recursively, then expose it via an API endpoint.

Recommendation

For a documentation site navigation sidebar, I'd recommend approach #1 (fetch flat + build client-side). It's the most flexible, performs well, and you only need to fetch the data once. The tree-building logic is straightforward and gives you complete control over rendering.

The page-tree plugin stores the hierarchy through parent references, so working with that flat structure is actually quite natural. You can also add features like collapsible sections, active path highlighting, and breadcrumbs more easily when you control the tree structure in your application code.

Show original thread
2 replies
GROQ can’t recursively resolve references, unfortunately.
Then i'll just limit the number of levels. Thanks

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?