Parent/child page relationships and nested slugs in Sanity

1 replies
Last updated: Jun 22, 2023
Hello, I've got a page document type which uses structured content for page building just pretty simple stuff for now. What I can't seem to figure out is the best way to approach parent / child relationships. I have a slug field like this
defineField({
  name: "slug",
  type: "slug",
  options: { source: "title" },
  group: "content",
}),
What I need to achieve is setting the relation and get the slug like the below.

Home
/
About
about
Guides
guides
> How To Play
guides/how-to-play
Slots
slots
> Best Slots
slots/best-slots

I can set the relation field but not sure on best approach for the slugs. If I manually set the Best Slots slug to be
slots/best-slots
in sanity studio it only comes through as
best-slots
are there any good guides on this?
Thanks
AI Update

I can help you set up hierarchical slugs for parent/child page relationships! This is a common pattern for building nested navigation structures.

The issue you're experiencing (where slots/best-slots becomes just best-slots) is because the slug field automatically sanitizes input by removing forward slashes and other special characters. To create hierarchical URLs, you need to generate the full path dynamically by combining a parent reference with the page's own slug.

Here's the recommended approach:

1. Add a Parent Reference Field

First, add a reference field to your page document type that points to other pages:

defineField({
  name: "parent",
  type: "reference",
  to: [{ type: "page" }],
  group: "content",
})

2. Keep the Slug Simple

Keep your slug field generating from just the page title (not the full path):

defineField({
  name: "slug",
  type: "slug",
  options: { source: "title" },
  group: "content",
})

So "Best Slots" would just have slug best-slots, not slots/best-slots.

3. Build the Full Path in Your Frontend

When querying your pages, construct the full path by recursively fetching parent slugs. Here's a GROQ query that does this:

*[_type == "page"] {
  _id,
  title,
  "slug": slug.current,
  "fullPath": select(
    defined(parent) => "/" + *[_id == ^.parent._ref][0].slug.current + "/" + slug.current,
    "/" + slug.current
  )
}

For more complex nested hierarchies (grandparent > parent > child), you'll need to handle this in your frontend code by recursively building the path, or use a more advanced approach with custom slugify functions.

4. Advanced: Custom Slugify with Parent Path

If you want the full path visible in Studio, you can create a custom slugify function that fetches the parent's slug using async operations. According to the slug field documentation, you can access the document context and use the Sanity client:

defineField({
  name: "slug",
  type: "slug",
  options: {
    source: "title",
    slugify: async (input, schemaType, context) => {
      const { parent } = context;
      const client = context.getClient({ apiVersion: '2024-01-01' });
      
      if (parent?._ref) {
        const parentDoc = await client.fetch(
          `*[_id == $parentId][0].slug.current`,
          { parentId: parent._ref }
        );
        
        if (parentDoc) {
          return `${parentDoc}/${input.toLowerCase().replace(/\s+/g, '-')}`;
        }
      }
      
      return input.toLowerCase().replace(/\s+/g, '-');
    }
  },
  group: "content",
})

Note that with this approach, if you change a parent's slug, child slugs won't automatically update - you'd need to handle that separately.

For most use cases, I'd recommend option #3 (building paths in the frontend) because:

  • It keeps your schema simple
  • Parent slug changes automatically propagate to children
  • It's easier to maintain and debug
  • You can implement breadcrumbs and navigation more easily

You can also check out this recipe on setting slugs with referenced field values and this answer about using reference fields to generate slugs for more advanced patterns.

Show original thread
1 reply
You could define a “parent” field for each document, like this
	defineField({
		name: 'parent',
		title: 'Kategori',
		type: 'reference',
		to: [{ type: "article" }],
	}),
Then have your slug field call a custom slugifier, like this:

	defineField({
		name: 'slug',
		title: 'URL',
	  	type: 'slug',
		options: {
			source: (doc, options) => ({ doc, options }),
			slugify: asyncSlugifier
		},
	}),
My slugifier function looks like this (slugifier.ts):


// Look up the slug of the parent category and append it to the slug of the current document
export async function asyncSlugifier(input: { doc: { title: string; parent: { _ref: string } } }, schemaType: any, context: any) {

	const {getClient} = context
	const client = getClient({apiVersion: '2023-03-01'})

	let pageSlug = input.doc.title
		.toLowerCase()
		.replace(/\s+/g, '-') // slugify the title
		.slice(0, 200);

	// Remove all non-alphanumeric characters (but keep the hyphens)
	pageSlug = pageSlug.replace(/[^A-Za-z0-9-]/g, '');

	if (input.doc.parent) {
		const query = '*[_id == $parentID]';
		const params = {parentID: input.doc.parent._ref}

		var result = await client.fetch(query, params);

		if (result.length > 0) {
			let parentSlug = result[0].slug.current;
			return `${parentSlug}/${pageSlug}`;
		} else {
			return pageSlug;
		}
	} else {
		return pageSlug;
	}	
}
This looks up the slug of the parent, and prepends that to the slugified title of the current document to make a new URL. This will work as long as the parent has it’s slug set correctly.

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?