Unique slugs for nested projects within clients in Sanity schema
Great question! This is a common pattern for client/project relationships. You'll want to use reference fields to connect projects to clients, and implement custom validation to ensure slug uniqueness within each client.
Schema Structure
Create two document types with a reference relationship:
// schemas/client.js
export default {
name: 'client',
type: 'document',
fields: [
{
name: 'name',
type: 'string',
validation: Rule => Rule.required()
},
{
name: 'slug',
type: 'slug',
options: {
source: 'name'
},
validation: Rule => Rule.required()
}
]
}
// schemas/project.js
export default {
name: 'project',
type: 'document',
fields: [
{
name: 'title',
type: 'string',
validation: Rule => Rule.required()
},
{
name: 'client',
type: 'reference',
to: [{type: 'client'}],
validation: Rule => Rule.required()
},
{
name: 'slug',
type: 'slug',
options: {
source: 'title'
},
validation: Rule => Rule.required().custom(async (slug, context) => {
const {document, getClient} = context
const client = getClient({apiVersion: '2023-01-01'})
if (!slug?.current || !document?.client?._ref) {
return true // Let required validation handle empty values
}
const query = `count(*[
_type == "project" &&
slug.current == $slug &&
client._ref == $clientRef &&
_id != $id
])`
const params = {
slug: slug.current,
clientRef: document.client._ref,
id: document._id
}
const count = await client.fetch(query, params)
return count === 0 ? true : 'This slug is already used by another project for this client'
})
}
]
}How It Works
- Projects reference clients using a reference field, which creates the parent-child relationship
- Custom validation on the project slug checks for duplicates within the same client using a GROQ query
- The validation allows
client-one/project-xandclient-two/project-xto coexist, but prevents duplicate slugs within the same client
Structure Tool Setup
To get a nice nested view in Studio, configure your structure builder:
// structure/index.js
export const structure = (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Clients')
.child(
S.documentTypeList('client')
.title('Clients')
.child((clientId) =>
S.documentList()
.title('Projects')
.filter('_type == "project" && client._ref == $clientId')
.params({clientId})
)
),
S.divider(),
...S.documentTypeListItems()
])This gives you a nested view where clicking a client shows their projects.
Querying for URLs
When you need to build URLs on your frontend:
*[_type == "project"] {
title,
"slug": slug.current,
"clientSlug": client->slug.current,
"fullPath": client->slug.current + "/" + slug.current
}The custom validation ensures your URL structure stays unique exactly as you described! The slug field type handles the URL-friendly formatting, and reference fields create the bidirectional relationship between clients and projects.
Show original thread18 replies
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.