🗓️ Everything *[NYC] is back. A free gathering for AI builders. Sept 9 →

Unique slugs for nested projects within clients in Sanity schema

18 repliesLast updated: Nov 29, 2025

Morning everyone 🙂
If I wanted to create a structure like this, how would I best do it?


Clients -> Projects
ie.

Client One
   - A Cool Project
   - Project Example
   - Project X
Client Two
   - Another Project
   - Project X
   - Something Else

Clients
would include a unique slug for the client name, and
Projects
would be nested under
Clients
, again with a unique slug.
The question mark for me is I don't want one
Client
to include projects with the same slug, as the URL would look like
client-one/code-veronica
so would need to be unique. However, another
Client
could include a matching slug, as it would still be unique in this case.
ie.

client-one/project-x
- Valid
client-one/project-x
- Invalid (matches previous)
client-two/project-x
- Valid
AI Update

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

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 thread
18 replies

Was this answer helpful?

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.

Related contributions