How to make hierarchical slugs in Sanity
September 10, 2021
A common way to structure pages on websites is a hierarchical folder-like pattern, aka root/child/grandchild
. To make that work you need a relationship from one page to another in the reverse order, i.e. child->parent. While this doesn't work out of the box with Sanity, they've made it easy to add it using a custom slugifier function. Here's how you can use that to programmatically build a hierarchical page slug.
Add a couple of fields to your schema definition
The first thing you need to do is to add a couple of fields to your page. The first one is a reference field that should point to the current page's parent. The second is the slug.
export default {
name: 'page', // or whatever your document name is
type: 'document', // this only works for documents
fieldsets: [
// fieldsets
],
fields: [
{
name: 'title', // this is what you'll base the slug on
type: 'string' // you can use other text-like types as well, but you may have to update the slugifier to match
},
{
name: 'parent',
type: 'reference',
to: [
{
type: 'page' // or whatever your document name is
}
]
},
{
name: 'slug',
type: 'slug',
options: {
source: (doc, options) => ({ doc, options }),
slugify: asyncSlugifier, // you'll define this in a minute
}
},
// your other document fields
]
}
For more information on the slug
type see the Sanity documentation.
As you can see, there's a reference to an undefined function named asyncSlugifier
. That's where the magic happens, so let's move on to that now.
Add the async slugifier function
Create a new function at the top of the file and name it asyncSlugifier
. It should take a single argument that contains the current document (if you want to change that up take a look at the source
field defined in the schema above) and return a string that will be used as the slug for the page.
import sanityClient from 'part:@sanity/base/client';
async function asyncSlugifier(input) {
const client = sanityClient.withConfig({
apiVersion: process.env.SANITY_STUDIO_API_VERSION || '2021-03-25', // Using the Sanity client without specifying the API version is deprecated
});
const parentQuery = '*[_id == $id][0]'; // a GROQ query, feel free to change this up to match what you need
const parentQueryParams = {
id: input.doc.parent?._ref || '',
};
const parent = await client.fetch(
parentQuery,
parentQueryParams,
);
const parentSlug = parent?.slug?.current ? `${parent.slug.current}/` : ''; // if there's no parent assign an empty string, it will make the function return the current slug as the root
const pageSlug = input.doc.title
.toLowerCase()
.replace(/\s+/g, '-') // slugify the title using a simple regex
.slice(0, 200);
return `${parentSlug}${pageSlug}`;
}
There's a bit going on here so let's take it step by step. You need a Sanity client (which is included with the studio) so you can make a query for the parent document and resolve its slug. When you have the parent you can move on to slugify the current page's title. Then simply return the parent slug and the page slug, et voilá, you have a page slug that can handle infinite hierarchies since it always builds on the parent slug. If the parent slug is root the page slug will be root/page
, if the parent slug is root/parent
the page slug will be root/parent/page
without any extra effort on your part.
Summary
So to recap:
- Add a parent reference field to the schema
- Add a slug field that points to a custom slugifier function
- Add a slugifier function that resolves the parent page reference and joins the slug of the parent with the slugified title of the current page.
That's it, there's nothing more to it. This is another example of why defining your schema in code is so good. It opens the door for solutions like the one above or other ways to build the page slug based on data from outside the document.