Parent/child page relationships and nested slugs in Sanity
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.
Recommended Pattern
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 thread1 reply
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.