Discussion on creating custom slugs for documents in Sanity and simplifying the process of turning references into links.

19 replies
Last updated: Apr 2, 2021
Curious 2: Do you store the full path in the Sanity slug – or just the slug of that document’s title?
I’m tired of constantly piecing together the slug logic on front and back ends -- so I’m writing a custom function/validator in Sanity to produce document slugs like
/office/sydney

They’re still valid in Sanity and it’ll make turning references into links simpler.
AI Update

You can store whatever you want in a Sanity slug field – it's just a string field with some helpful generation features built in. By default, Sanity only stores the document's own slug (like sydney), not the full path. But there's nothing stopping you from storing full paths like /office/sydney in the slug field.

Your approach of storing the complete path in the slug is totally valid and can definitely simplify your routing logic! Here's what you should know:

Storing Full Paths

The slug field type stores whatever string you put in slug.current. You can absolutely store /office/sydney instead of just sydney. This works fine in Sanity and can make your frontend routing much cleaner since you won't need to reconstruct paths from references.

Custom Slugify Function

You can use the slugify option to implement your custom path logic. Here's a basic example:

{
  name: 'slug',
  type: 'slug',
  options: {
    source: (doc) => {
      // Your logic to construct the full path
      if (doc.parent?._ref) {
        // Fetch parent slug and prepend it
        return `/office/${doc.title}`
      }
      return doc.title
    },
    slugify: (input) => {
      // Custom slugification that preserves your path structure
      return input
        .toLowerCase()
        .replace(/\s+/g, '-')
        .replace(/[^\w\-\/]/g, '') // Keep slashes!
    }
  }
}

Important Considerations

isUnique validation: If you're using the isUnique validator, be aware that it checks uniqueness across all documents of that type. With full paths, this should work fine since each path should be unique anyway.

References and hierarchy: If you're building paths based on parent-child relationships (like /office/sydney where "sydney" is a child of "office"), you might need to handle updates when parent slugs change. The slugify function only has access to the current document, so for reference-based paths, you may need to use a custom input component or document action to fetch and construct the full path.

The Trade-off

The downside of storing full paths is that if you ever restructure your content hierarchy or rename a parent, you'll need to update all child document slugs. With just storing the document's own slug, you'd reconstruct paths at query time, which automatically reflects hierarchy changes.

But if the simplicity of having ready-to-use paths in your frontend outweighs this maintenance concern (which it often does!), then go for it. Many developers find this approach cleaner than constantly piecing together paths across their codebase.

In 90% of the cases, I tend to translate
_type
 (or some other property [e.g.
category
]) to the first segment. However, in the community studio you can find a custom slug component that supports the
basePath
 option: https://github.com/sanity-io/community-studio/blob/dc1ed97818f2a5397e982bac99dbf8acb8cf9128/schemas/components/PathInput.js
That’s really cool. For now I’m getting that sort of behaviour just using a function to wrap a normal
slug
field:

slugWithType([`contact`], `title`),

…which is rendering something like this
That slug input is now a plugin using
@sanity/ui
w/ support for the generate function: https://www.sanity.io/plugins/better-slug 🙌
…bringing this discussion back here
I’ve settled on this, for now
😄

https://gist.github.com/SimeonGriggs/c4c66bc5f6aacea96788248d09862926
Main benefit I see is slugs === links
I'm curious: how do you fetch those in the front-end? As you're using next, it returns the slug with the slash...Do you prepend a leading slash to the GROQ query when looking for the document related to that slug? (aka: `client.fetch('*[slug == $slug]', slug:
/${slug}
)`)
😄
That bottom section before was a 4-step switch statement to generate the right paths – which then need to be reassembled and disassembled all over again.
Now the fetching is simpler, as is every instance of creating a link to a page throughout the site.
…and actually, the query no longer needs to be a keyed object either, because the only reason I was doing that was to generate paths in the switch statement. It could be even simpler!!
But now, without a
.reduce
or a switch statement how will anyone know how cool I am? 😂
user S
you're doing what I always want to do but never have the courage with Next: using a single
[..slug]
root-level route and connecting it to multiple templates, right?
My issue with this is that it usually leads to including a lot of JS & CSS for other templates which aren't used by a given page, hurting performance. Have you found a way to go beyond that? Would love to learn
😀
I’m not at the perf optimisation stage yet … maybe ask me later 😄
user B
If you’re using something like Emotion, you shouldn’t wind up with any extra CSS being built into the page for components that don’t wind up producing HTML markup. As for the extra JS from unused components, can you not use dynamic imports ?
Actually yeah I use tailwind so it’s only the one css file for the whole project.
And I’m meaning to look at dynamic imports in my page builder array...eventually
You can do something like this, guys.

import dynamic from "next/dynamic";

const modules = {
  HelloWorldModule: dynamic(() => import("./hello-world")),
};

export default modules;
And then:


const module = modules[moduleName]
How do you handle props in this instance
user L
?
Actually, got it!

import Hero from './Hero'

const dynamicBlocks = {
  pageBuilderQuote: dynamic(() => import('./Quote')),
  /// ...etc
}

const staticBlocks = {
  pageBuilderHero: (block) => <Hero {...block} />,
}

export default function PageBuilder({ blocks }) {
  return blocks
    .map((block) => ({ key: block._key, ...block }))
    .map((block) =>
      dynamicBlocks[block._type]
        ? React.createElement(dynamicBlocks[block._type], block)
        : staticBlocks[block._type](block)
    )
}
user S
It is easier than that, actually. (This is a minimal version. Probably, you could want to check for more things including but not limited to environment, check if module exits, etc.)

import dynamic from "next/dynamic";

const modules = {
  HelloWorldModule: dynamic(() => import("./hello-world")),
};

export default function Builder({ modules = [] }) {
  return modules.map(({ __typename, ...moduleProps }) => {
    const Module = modules[__typename];
    return <Module {...moduleProps} />;
  });
}

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?