How to avoid nested parent queries and generate full slug path in Sanity?

5 replies
Last updated: Mar 10, 2022
Hi everyone, is it possible to create a loop within a query to avoid the nesting I'm doing with the parent below?Basically it's checking for a parent reference on the parent page so we can generate a URL.

Also wondering if it's possible to create an output within the query that would join all of these together like "fullSlug":
parent.slug/ parent.slug/slug.current
`const link = ``

title,

url,

"page": page->{

"type": _type,

"slug": slug.current,

"parent": parent->{

"slug": slug.current,

"id": _id,

"parent": parent->{

"slug": slug.current,

"id": _id,

"parent": parent->{

"slug": slug.current,

"id": _id,

"parent": parent->{

"slug": slug.current,

"id": _id

}

}

}

},

},
AI Update

Unfortunately, GROQ doesn't have built-in loops or recursive traversal capabilities, so you can't avoid the manual nesting you're currently doing if you need to traverse an unknown depth of parent references. However, there are some practical approaches to handle this:

The Reality: No Native Recursion

GROQ doesn't support recursive queries or loops. The reference access operator (->) resolves one level at a time, so you need to manually nest the parent-> chains as you're doing. If you need to support arbitrary depth, you'll need to handle this in your application code rather than in the query itself.

Better Approaches

1. Build the query programmatically (recommended for your case)

Based on this similar question from the Sanity community, the best approach is to build your GROQ query dynamically in JavaScript before executing it:

const slug = [null, 'parent', 'page']
const parentPageSelector = `parent->`;
const idQuery = slug
  .map(
    (_segment, index, slug) =>
      `${parentPageSelector.repeat(index)}slug.current == $slug[${slug.length - 1 - index}]`
  )
  .join('&& \n');

const query = `*[${idQuery}]`;

This generates the nested query structure you need based on your actual URL depth.

2. Build the full path in application code

Query with a reasonable nesting depth (4-5 levels), then recursively traverse the parent chain in JavaScript:

function buildFullSlug(page) {
  const slugs = [];
  let current = page;
  
  while (current) {
    if (current.slug) {
      slugs.unshift(current.slug);
    }
    current = current.parent;
  }
  
  return slugs.join('/');
}

3. Denormalize with a computed field

Consider adding a fullSlug field to your documents that gets computed when you save. You could use Sanity Functions with a document change event handler to automatically rebuild the full slug whenever a page or any of its ancestors changes. This is the most performant option for read-heavy applications since you compute once and query many times.

Joining Strings in GROQ

Regarding your question about joining slugs in the query itself - GROQ does have string manipulation functions available in the GROQ Functions reference, but they only work on arrays of strings at the same level. You can't use them to traverse nested parent relationships. For example, if you had a flat array of slugs, you could do:

string::join(arrayOfSlugs, "/")

But since your slugs are in nested parent objects, you'd need to first collect them into an array, which GROQ can't do recursively.

For your use case, I'd recommend:

  1. Build your GROQ query programmatically using string.repeat() as shown above - this handles variable depth cleanly
  2. Or compute the fullSlug as a field on your documents using Sanity Functions to avoid computing it on every request - best for performance
  3. Keep your manual nesting to a reasonable depth (4-5 levels) as a fallback if you prefer pure GROQ

This gives you the best balance of query performance and flexibility without fighting against GROQ's limitations. The programmatic query building approach is actually quite elegant and handles the breadcrumb/full slug path problem nicely!

Show original thread
5 replies
Thanks User, I thought that was the case but wasn't sure if I was missing something.Also just confirming that there isn't a smarter way that I could output the fullslug within the query as a variable without writing a separate function?
The only suggestion I would make would be that you may want to handle the compounding of the different slugs within the Studio itself, similar to this guide.
But if that's not an option I'm not sure that there is a cleaner way to GROQ it out.
great, thanks
user M
that is what I'm looking for
Glad to help! Let us know if you hit any more snags!
user M
if it helps anyone else, I've written the function from the docs to allow for endless nesting. Is it possible to extend the "onAfterPublish" function so that we could automate this if a parent updates their slug, to then update all children without it being a manual process?

async function getPrefix(doc, source, ref) {

const docTitle = doc[source]

`// const refQuery = `*[_id == $ref][0].title``

const refParams = {ref: doc?.[ref]?._ref}


if (!refParams.ref) {

return slugify(docTitle, slugifyConfig)

}


// const childTitles = [docTitle];

const refTitles = await getParentTitles(doc);


if (!refTitles) {

return slugify(docTitle, slugifyConfig)

}


const slugArray = refTitles.filter((p) => p).map((p) => slugify(p, slugifyConfig))


return slugArray

}


async function getParentTitles(doc, childTitles){

if(childTitles){

childTitles.unshift(doc.title);

} else {

childTitles = [doc.title];

}


// Fetch the parent pages

if( doc.parent ){

const ref = doc.parent._ref;

const params = {ref: ref};
`const query = `*[_type == "page" && _id == "${ref}"][0]{"title": title, "parent": parent->{"_ref": _id, "title": title}}`;`

const foundDoc = await client.fetch(query, params);

if( foundDoc !== undefined ){

childTitles = await getParentTitles(foundDoc, childTitles);

}

}


return childTitles;

}

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?