How to make nested parent page slug query more generic in Sanity?

10 replies
Last updated: Jun 9, 2022
hi, can this query be written in a more generic way?query:

*[
  slug.current == $slug[2] &&
  parentPage->slug.current == $slug[1] &&
  parentPage->parentPage->slug.current == $slug[0]
 ]
params:

{
  "slug": ["/","parent", "page"]
}

AI Update

Looking at your query, yes, it can definitely be written in a more generic way! Your current approach hardcodes the depth (3 levels) and array indices. Here are several approaches to make it more flexible:

The Challenge

GROQ doesn't support recursive traversal of parent references at arbitrary depth. You can't write something like parent->parent->... dynamically. However, there are some practical solutions depending on your use case:

Solution 1: Use Array Slice with Negative Index

If you know the slug path structure, you can work with the array more flexibly:

*[
  slug.current == $slug[-1] &&
  parentPage->slug.current == $slug[-2] &&
  parentPage->parentPage->slug.current == $slug[-3]
]

This uses negative indexing to access elements from the end of the array, making it slightly more readable.

Solution 2: Build a Path String for Comparison

If your documents have a computed path field or you can build one, you could do:

*[
  path == "/" + array::join($slug[1..], "/")
]

This assumes you have a path field that stores the full hierarchical path.

Solution 3: Use a Custom GROQ Function

For truly dynamic depth traversal, you could create a custom GROQ function (requires Growth plan or higher). This would allow you to define reusable logic for traversing parent chains.

The most practical solution for deep hierarchies is to denormalize by storing an ancestors array on each document:

{
  slug: "page",
  ancestors: [
    { _ref: "root-id" },
    { _ref: "parent-id" }
  ]
}

Then your query becomes much simpler and works at any depth:

*[
  slug.current == $slug[-1] &&
  count(ancestors[@._ref in $ancestorIds]) == count($ancestorIds)
]

The Reality

Unfortunately, GROQ does not support arbitrary-depth recursive parent traversal. The language is intentionally designed without recursion to maintain performance and predictability. As explained in the How Queries Work documentation, your current approach of explicitly chaining parent->parent-> is actually the standard pattern when you know the depth.

If you need to support variable depths, denormalizing with an ancestors array is the recommended Sanity pattern. This also significantly improves query performance for deep hierarchies since you avoid multiple reference lookups.

Would you like help implementing the ancestors approach? It typically involves adding a field to your schema and using a migration or document action to populate it when documents are created or updated.

Show original thread
10 replies
I just did a fun experiment where I iterated through the param with

$slug[] { "slug" : ^ }
and nothing else, and it looped through the values at least, but I can't think of a way to not only fetch the index automatically but add cascading amounts of parents
with dereferencing.
I think the closest you could get is writing it programmatically on a loop, concatenating them to
form the query when developing and letting it run that. I'm no expert, though.
What constitutes a parent page in this case? Is there an actual hierarchy? Or is it designated?
i have route schema and in there, you can define another route as the parent page, to create a hierarchy of pages…

export default {
  name: "route",
  type: "document",
  title: "Route",
  icon: LinkIcon,
  fields: [
    {
      name: "parentPage",
      type: "reference",
      to: {
        type: "route",
      },
    },
    {
      name: "slug",
      type: "slug",
      title: "Slug",
    },
  ]
}

Hmm. I know there are operands(?) like ^ for parent and @ for "this" but I don't think there's support for recursion or forcing dereferencing down to a certain depth. Making a schema for routes is clever, though.
ok… so it looks like an feasible option is to iterate over the array and create and concatenate the query before sending it
Awesome. Yeah ideally you'd be able to tee yourself automagically with just the one move but it's basically infinite power to combine them and make the formation of the query intelligent.
Hi, in this particular case the best solution is probably to build the GROQ query programmatically from string fragments.
ok, this is my solution now… a little bit of a FrankenGroq but it works 🙂
const slug = [null, 'parent', 'page']
const parentPageSelector = `parentPage->`;
const idQuery = slug
    .map(
        (_segment, index, slug) =>
            `${parentPageSelector.repeat(index)}slug.current == $slug[${slug.length - 1 - index}]`
    )
    .join('&& \n');

const query = `*[${idQuery}]`;
Oh man, breaking out the repeat() ...good times 😃
haha… yes… i have not used
string.repeat()
in ages
That's one of those ones where you're in a corner and you're like, "Wait, there's gotta be something that does that" and that's the first day you use it and, ideally, the only time...but it finds a way back!

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?