Custom URL validator timing out when checking slugs against published documents

17 replies
Last updated: Jan 28, 2026
Any advice on a custom URL validator? I want to ensure that any internal links point to published slugs. It works, but slowly. I keep getting this warning:

Custom validator at linkGroups[_key=="721a4739faa3"].links[_key=="e61f9228310a"].linkUrl has taken more than 5000ms to respond

here’s the code:


import client from "part:@sanity/base/client";

const listAllSlugs = async () => {
  const slugsList = await client
    .withConfig({ apiVersion: "2021-10-21" })
    // require slug and exclude unpublished drafts
    .fetch(
      `*[defined(slug.current) && !(_id in path("drafts.**"))][].slug.current`
    );

  return slugsList;
};

export const validateLink = (url) => {
 
  // if it's an internal link
  if (url.startsWith("/")) {
    // remove # and ?
    const clean = url.split("#")[0].split("?")[0];

    return listAllSlugs().then((allSlugs) => {
      return allSlugs.includes(clean)
        ? true
        : "Does not match any published slugs";
    });
  }
  // ... test against http url patterns ...
};
AI Update

Your validator is slow because it fetches all slugs from your entire dataset on every keystroke. The solution is to cache the slug list and reuse it across validation calls.

Here are a few strategies to speed this up:

1. Cache the slug list outside the validator function

The simplest fix is to fetch slugs once when the Studio loads and reuse that cached list:

import client from "part:@sanity/base/client";

// Cache slugs at module level
let slugsCache = null;
let cachePromise = null;

const getAllSlugs = () => {
  // If we already have slugs, return them
  if (slugsCache) return Promise.resolve(slugsCache);
  
  // If a fetch is already in progress, return that promise
  if (cachePromise) return cachePromise;
  
  // Otherwise, start a new fetch
  cachePromise = client
    .withConfig({ apiVersion: "2021-10-21" })
    .fetch(`*[defined(slug.current) && !(_id in path("drafts.**"))].slug.current`)
    .then((slugs) => {
      slugsCache = slugs;
      cachePromise = null;
      return slugs;
    });
  
  return cachePromise;
};

export const validateLink = (url) => {
  if (url.startsWith("/")) {
    const clean = url.split("#")[0].split("?")[0];
    
    return getAllSlugs().then((allSlugs) => {
      return allSlugs.includes(clean)
        ? true
        : "Does not match any published slugs";
    });
  }
  // ... test against http url patterns ...
};

This caches the result in memory, so subsequent validations are instant. The cache persists until the Studio reloads.

2. Use a listener to keep the cache fresh

For a more robust solution, listen to document changes and update your cache:

import client from "part:@sanity/base/client";

let slugsCache = new Set();

// Initial fetch
client
  .withConfig({ apiVersion: "2021-10-21" })
  .fetch(`*[defined(slug.current) && !(_id in path("drafts.**"))]{_id, "slug": slug.current}`)
  .then((docs) => {
    slugsCache = new Set(docs.map(d => d.slug));
  });

// Listen for changes
client
  .listen('*[defined(slug.current)]')
  .subscribe((update) => {
    if (update.type === 'mutation') {
      // Refetch or update cache intelligently
      // For simplicity, you could just refetch all slugs
    }
  });

export const validateLink = (url) => {
  if (url.startsWith("/")) {
    const clean = url.split("#")[0].split("?")[0];
    return slugsCache.has(clean) ? true : "Does not match any published slugs";
  }
};

3. Use a reference field instead

Consider whether you actually need URL validation, or if a reference field would work better. References give you a document picker and ensure links always point to real documents:

{
  name: 'internalLink',
  type: 'reference',
  to: [{type: 'page'}, {type: 'post'}] // whatever doc types have slugs
}

Then resolve the slug when querying. This is type-safe and doesn't require validation at all.

Why your current code is slow

Every time someone types in the URL field, your validator:

  1. Makes a network request to Sanity
  2. Fetches potentially thousands of documents
  3. Extracts all slugs
  4. Checks if the typed value matches

With caching, you do steps 1-3 once, and step 4 becomes instant.

The 5000ms warning appears because Sanity expects validators to respond quickly (they run on every keystroke). Your approach works correctly, it's just the performance that needs optimization.

Show original thread
17 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.

Was this answer helpful?