
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeGreat 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.
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'
})
}
]
}client-one/project-x and client-two/project-x to coexist, but prevents duplicate slugs within the same clientTo 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.
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.
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store