Unique slug validation per document type in Sanity

4 replies
Last updated: Mar 29, 2024
I'm trying to implement a unique slug validation, but I only need them unique among the same doc type (name, document._type value). The example function for this is across all documents. Does anyone have an idea how to add the type to this? (details in) 🧵
AI Update

By default, Sanity's slug field already checks for uniqueness only within documents of the same type (based on document._type). So if you're using the standard slug field without a custom isUnique function, you don't need to do anything—slugs are automatically scoped to the document type.

However, if you've implemented a custom isUnique function (like the example in the Sanity slug documentation that checks across all documents), you'll need to add the _type constraint to scope it back to the same document type.

Here's how to modify a custom isUnique function to check uniqueness only within the same document type:

// lib/isUniqueWithinType.ts
import { getPublishedId } from 'sanity';

export async function isUniqueWithinType(slug, context) {
  const { document, getClient } = context;
  const client = getClient({ apiVersion: '2025-02-19' });
  const id = document?._id;
  const type = document?._type;

  if (!id || !slug?.current || !type) {
    return true;
  }

  const publishedId = getPublishedId(id);

  const params = {
    published: publishedId,
    slug: slug.current,
    type: type, // Add the document type
  };

  // Add _type constraint to the query
  const query = groq`!defined(*[
    _type == $type &&
    !sanity::versionOf($published) &&
    slug.current == $slug
  ][0]._id)`;

  const isUnique = await client.fetch(query, params);
  return isUnique || false;
}

Then use it in your schema:

import { isUniqueWithinType } from '../lib/isUniqueWithinType';

export default {
  name: 'post',
  type: 'document',
  fields: [
    {
      name: 'slug',
      type: 'slug',
      options: {
        source: 'title',
        isUnique: isUniqueWithinType
      }
    }
  ]
}

Key changes:

  1. Extract document._type from the context
  2. Add _type == $type to the GROQ query to filter only documents of the same type
  3. Pass the type as a parameter to the query

This ensures slugs only need to be unique within documents of the same _type value (e.g., all post documents can have unique slugs, all page documents can have unique slugs, but a post and a page can share the same slug).

Show original thread
4 replies
This is the example I have been using, taken right from the docs here
// /lib/isUniqueAcrossAllDocuments.js

// Note: this assumes that every document that has a slug field
// have it on the `slug` field at the root
export async function isUniqueAcrossAllDocuments(slug, context) {
  const {document, getClient} = context
  const client = getClient({apiVersion: '2022-12-07'})
  const id = document._id.replace(/^drafts\./, '')
  const params = {
    draft: `drafts.${id}`,
    published: id,
    slug,
  }
  const query = `!defined(*[!(_id in [$draft, $published]) && slug.current == $slug][0]._id)`
  const result = await client.fetch(query, params)
  return result
}
But the update I am trying to make I cannot get dialed in. Basically I need to only enforce this if both documents are the same type. So if it s a 'presentation' and a 'video' (name values) and the url is the same that does not matter to me, only if both are 'video' (or whatever the template name is)
The behaviour you’re explaining (uniqueness across document types) should be the default. Getting rid of this function should get you what you’re after.
Hi there! I'm using this validation code below, but I have a difference in the return statement compared to yours. While you exclude draft documents and return an empty array, mine return a boolean value (
true
or
false
). Here's my code for your reference:

defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'title',
        maxLength: 96,
        isUnique: async (slug, context) => {
         // Search for all documents with the same slug
          const query = `*[_type == "post" && slug.current == $slug]`;
          
          const documents = await context.getClient({ apiVersion }).fetch<Post[]>(query, {
            slug,
          });
          // Returns true if no documents are found, false otherwise
          return documents.length <= 1;
        },
      },
    }),
user A
Did not even realize that, thanks for pointing that out, worked exactly how I was expecting it too, thanks!

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.

Was this answer helpful?