Recursive query for page-tree plugin navigation with dynamic depth
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 thread2 replies
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.