How to add document _id to slug field using custom slugifier function

6 replies
Last updated: Aug 14, 2022
I’m stuck at trying to write a slugifier that adds the current document’s
_id
value to a slug field. My intention is to have a slug that is created based on a couple of values from the same document but I can only have a single source, correct? I’m seeing that a function can also be used as the source, but I’m kinda lost . What would be the best approach? Is there a way to add the
_id
(also slicing the first 7 characters to remove ‘drafts.’)
AI Update

You're on the right track! You can absolutely use a function as the source option in a slug field, and this is the perfect way to combine multiple values including the _id. While the slug field technically takes a single source, that source can be a function that returns a string combining whatever fields you need.

Here's how to set this up to include the _id while removing the drafts. prefix:

{
  name: 'slug',
  type: 'slug',
  options: {
    source: (doc) => {
      // Remove 'drafts.' prefix from _id if it exists
      const cleanId = doc._id?.replace(/^drafts\./, '') || '';
      
      // Combine with other fields as needed
      return `${doc.title}-${cleanId}`;
    },
    maxLength: 96,
    // Optional: customize the slugify function if needed
    slugify: (input) => input
      .toLowerCase()
      .replace(/\s+/g, '-')
      .replace(/[^\w-]+/g, '')
  }
}

The key points here:

  1. Source function: The source option accepts a function that receives the current document (doc) as a parameter, giving you access to all fields including _id
  2. Removing drafts prefix: Use replace(/^drafts\./, '') to strip the drafts. prefix from the beginning of the _id
  3. Combining values: Return a string that combines whatever fields you need - title, date, _id, etc.

If you want to slice just the first 7 characters after removing the prefix, you could do:

source: (doc) => {
  const cleanId = doc._id?.replace(/^drafts\./, '').slice(0, 7) || '';
  return `${doc.title}-${cleanId}`;
}

The slug field documentation covers the basics, and as mentioned in the Sanity community answers about using multiple fields as sources, this function approach is the standard way to combine multiple document values into a single slug.

One thing to keep in mind: the slug generation happens when you click the "Generate" button in Studio, so the _id needs to exist at that point (which it will for existing documents, but new documents won't have an _id until after the first save).

Show original thread
6 replies
Hi Guillermo. When you write a function for your slug source, it can include conditional logic or multiple sources. With a few more details on what you’re after, we could elaborate.
To use the
_id
as you asked, you could do something like:

{
	name: 'slug',
	title: 'Slug',
	type: 'slug',
	options: {
		source: (doc) => {
			return doc?._id ? doc._id.replace(/^drafts./,'') : ''
		}
	}
}
A brand new document won’t have an
_id
set, so you may want to handle that situation better. In the code above, you’d have to press the “Generate” button twice (once to give the document an
_id
, and again to generate the slug).
Hi
user A
. That’s exactly what I was trying to achieve. I’ll give a bit more background. I’m creating payment tickets, for which the schema generates a slug field that can be copied and then emailed to our clients. The current slug takes in
orderId
as an input, and creates a slug in the following fashion:
<http://mydomain.com/store/ticket?orderId=768766|mydomain.com/store/ticket?orderId=768766>
where ‘768766’ is the value used as an identifier.
However, this presents a problem as anyone with the link can simply try different numbers and see other active tickets (which show client’s name and details) and obviously I don’t want that.

Sooo, the solution I came up with was to have a unique identifier in the URL to use as a challenge. Since the
_id
is unique for every document, I thought about adding that to the URL, like so:

<http://mydomain.com/store/ticket?orderId=768766&challenge=72117b40-b0b8-4c9f|mydomain.com/store/ticket?orderId=768766&challenge=72117b40-b0b8-4c9f>

This way I can capture both URL params and pass them to my query . If they match, I show the ticket details. There’s no way anyone will be able to guess that pair of values. Does it make any sense?
Thanks for that extra detail. I’m not too sure about best practices around slugs and query parameters like that, but I’m sure someone else will pick that question up. Have a good weekend!
Thank you! I think that having an extra url param to check if it matches other values is standard, at least it sound logical to me.
I guess I can just use the
_id
instead of trying to match two values. Might be easier.
user N
I can't vouch for best practices either, but in some versions of Woocommerce the order page's URL param is a similar approach to yours with just a single param derived from the order ID while not citing it directly.
If you know the secret sauce in the Studio and in the front end (perhaps salted by an environment variable) you could generate something so elaborate that nobody is realistically going to arrive at it randomly, but you can descramble it as necessary to pull up the actual decent and normal ID.

Like my

<http://domain.com/store/ticket?orderId=puX6xKvKjKVDrdOxdyIrgHHrb9imw1Vza|domain.com/store/ticket?orderId=puX6xKvKjKVDrdOxdyIrgHHrb9imw1Vza>
where you ran the order ID through a cryptographic function using a specific a single secret word of phrase to inform the scrambling as a separate parameter.

The Studio would ideally just be you and your colleagues so it's less of an issue if the secret phrase is known if hunted for in the source, and then in the front end the formula could be in the source but the phrase stored in an environment variable.

So the Studio would have the encrypt function using it, and the front end could have the decrypt where it's descrambling with the most important bit that nobody can see.

I used to use a version of that approach to secure documents in PHP, but what I would do is the code would reference the true details (like a doc ID or a filename) and stick that as a data attribute in the markup, then if someone clicked the resulting markup it would attempt a fetch for another function (which also knew the secret) and attempt to validate it.

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?