Validating unique slugs with parent pages in Sanity hierarchical structure

11 replies
Last updated: Dec 15, 2022
Hi everyone, I have a client website with alot of pages, most have parent pages which will alter the slug so technically they are all different but the page slug itself may be duplicated across pages e.g. a page will have a slug of 'north-east' but they will all have different parents. I've tried to change the validation to return true if the there is only 1 occurence of the slug with a parent but it still returns the error of 'Slug is already in use' Any ideas how I can get around this?
Here is my current slug, parentPage code:


{
			name: 'slug',
			title: 'Slug',
			type: 'slug',
			options: {
				source: 'name',
				slugify: (input) =>
					input
						.toLowerCase()
						//Remove spaces
						.replace(/\s+/g, '-')
						//Remove special characters
						.replace(/[&\/\\#,+()$~%.''":*?<>{}]/g, ''),
			},
			validation: Rule => Rule.custom((slug, section) => {
				console.log(slug, section.parent.parentPage._ref)
				let parentRef = '*[_type=="pillar" && slug.current == $slug]{parentPage{_ref}}'
				const params = {slug: slug.current}
				return sanityClient.fetch(parentRef, params).then(matchingParents => {
					let count = 0;
					console.log({matchingParents})
					matchingParents.forEach(page => {
						page.parentPage._ref === section.parent.parentPage._ref ? count++ : count
					})

					console.log({count})
					if(count === 1) {
						return true
					} else {
						return false
					} 
				})
			})
		},

		{
			name: 'parentPage',
			title: 'Parent Page',
			type: 'reference',
			to: [{ type: 'pillar' }],
			// This ensures we cannot select other "children"
			options: {
				filter: '!defined(parentPage)',
			},
		},
AI Update

Looking at your code, I can see the issue. You're trying to use custom validation on the slug field, but Sanity has a specific isUnique option for slug fields that should be used instead. The built-in uniqueness check is still running and causing the error.

Here's how to fix it:

Create a separate isUnique function:

// lib/isUniqueSlugWithParent.js
export async function isUniqueSlugWithParent(slug, context) {
  const { document, getClient } = context;
  const client = getClient({ apiVersion: '2025-02-19' });
  
  const id = document?._id;
  const parentRef = document?.parentPage?._ref;
  
  // If no slug or no ID, consider it valid
  if (!slug?.current || !id) {
    return true;
  }

  // Remove draft prefix if present
  const publishedId = id.replace(/^drafts\./, '');

  // Check if any OTHER document has the same slug + parent combination
  const query = `count(*[
    _type == "pillar" && 
    slug.current == $slug && 
    parentPage._ref == $parentRef &&
    !(_id in [$id, $draftId])
  ])`;

  const params = {
    slug: slug.current,
    parentRef: parentRef,
    id: publishedId,
    draftId: `drafts.${publishedId}`
  };

  const count = await client.fetch(query, params);
  return count === 0; // Return true if unique (count is 0)
}

Update your schema to use the isUnique option:

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

{
  name: 'slug',
  title: 'Slug',
  type: 'slug',
  options: {
    source: 'name',
    slugify: (input) =>
      input
        .toLowerCase()
        .replace(/\s+/g, '-')
        .replace(/[&\/\\#,+()$~%.''":*?<>{}]/g, ''),
    isUnique: isUniqueSlugWithParent  // Use this instead of validation
  }
  // Remove the validation property entirely
},

{
  name: 'parentPage',
  title: 'Parent Page',
  type: 'reference',
  to: [{ type: 'pillar' }],
  options: {
    filter: '!defined(parentPage)',
  },
}

Key differences from your code:

  1. Use isUnique option instead of validation - The slug field has a special isUnique callback that overrides the default uniqueness check
  2. The query checks the right thing - It looks for documents with the same slug AND same parent, excluding the current document (both draft and published versions)
  3. Return boolean directly - isUnique expects true (unique) or false (not unique), unlike validation functions that can return error messages
  4. Handle drafts properly - The query excludes both the published and draft versions of the current document

This will allow you to have multiple pages with slug "north-east" as long as they have different parent pages. The uniqueness is now scoped to the combination of slug + parent page.

Show original thread
11 replies
I also tried using
isUnique
but it then didn't do any validation, this one has me stumped!
Bumping
If the above isn't possible, is there a way to completely remove validation on a slug or would it have to be a string field instead?
You may just be returning a promise. It also might be an issue that you're returning false instead of a string.
validation: Rule => Rule.custom(async (slug, section) => {
				const query = '*[_type=="pillar" && slug.current == $slug].parentPage._ref'
				const params = {slug: slug.current}
				const parentRefs = await sanityClient.fetch(query, params)

        return parentRefs.filter(parentRef => parentRef === section.parent.parentPage._ref).length === 1 ? true : 'This slug is already in use'
        
			})
Hi
user M
thanks for your help, I'm still learning my way around groq queries! Unfortunately when I used the above code I'm getting an error of
flushSync was called from inside a lifecycle method. It cannot be called when React is already rendering.
If I click on the tooltip at the top of the document it gives the original error of 'Slug is in use' and when I hover over the tooltip beside the slug field it causes the flushSync error, any ideas?
If I click on the tooltip at the top of the document it gives the original error of 'Slug is in use' and when I hover over the tooltip beside the slug field it causes the flushSync error, any ideas?
is there a way to override the original slug validation that comes with sanity?
Lookup isUnique method
brilliant, thank you! I was doing validation or isUnique, turns out I needed them both! Thank you so much, this was driving me crazy!

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?