This is a slug. Check it out, just slugging along.

And this is also a slug.

See the similarities? Yeah, me neither - the similarity starts and ends with their name: slug.
The slug we'll be focusing on is the second, and it's actually a lot more important than you might think. A slug is a uniquely-specified string apart of your URL that helps us locate specific pages and documents.
If you go to some of your favorite sites, you'll see slugs everywhere! For our blog post, we want pages for each of our posts, and slugs help us do that by providing a URL-friendly string that we can use to uniquely identify the post we want.
Adding a slug field
Adding a slug field is the same as adding any other field:
{ name: 'viewCount', ...},{ name: 'slug', type: 'slug', title: 'Slug', group: 'metadata',}...
You'll see a Slug field pop up in your Studio, but we need to do a bit more - right now it's basically a String.
We're going to make two updates:
- Remove our regex validation from our title field
- Have our slug automatically generate based on the content in our title field
Why are we removing our regex from the title field? Because now that we have a slug to uniquely identify our content, it doesn't really matter how our title is formatted. (And we'll take care of it automatically.)
...{ name: 'title', type: 'string', title: 'Title', group: 'content', validation: (Rule) => Rule.required().max(50).min(10),},...,{ name: 'slug', type: 'slug', title: 'Slug', group: 'metadata', validation: (Rule) => Rule.required(), options: { source: 'title', maxLength: 96, },}...
Let's break down what we added to the slug schema field:
- We added a required validation to make sure this schema field is filled out
- Added a new options field inside of our slug with two more fields:
- source: This defines which field we want to use to make our Slug from - it makes the most sense to choose our title
- maxLength: This is the maximum length we want our slug to be. For URLs, if we can keep them short and readable, we should.
Go back to Studio, we can check out our Blog Post that we have so far. Now that we've removed the regex validation from the title, let's go it bit crazy with some URL-unfriendly characters.
Then, go ahead and hit the Generate button thats next to the Slug field:

And boom - a URL that's friendly and readable to the eye. Let's head to Vision and try using our new Slug.
If you're wondering how the fields are next to each other in the picture, I just moved them. You can rearrange your fields in your schema anyway you see fit - just make sure you don't accidentally delete a field in the process. (Even if you do, Sanity will notify you that data exists without a field.)
Slugs in Data
Let's use the following query in Vision and see the result:
*[_type == "blogPost"]
[…] 1 item 0:{…} 9 properties _createdAt:2023-02-11T09:05:20Z _id:16488aca-cafc-49df-8484-db9d8c1f5caa _rev:EmCriQJSDEYU7ED0BiACPF _type:blogPost _updatedAt:2023-02-12T08:59:11Z description:And this is my first blog post's description. Kinda great, huh? slug:{…} 2 properties _type:slug current:this-is-my-first-blog-post-can-t-you-believe-it title:This is my first blog post! Can't you believe it?? viewCount:1
We can see our title, description, view count, and our slug field!
Go back to the Desk view, and let's add another blog post.

If we go back to Vision and refetch the same query, we'll get a new result:
[…] 2 items 0:{…} 9 properties _createdAt:2023-02-11T09:05:20Z _id:16488aca-cafc-49df-8484-db9d8c1f5caa _rev:EmCriQJSDEYU7ED0BiACPF _type:blogPost _updatedAt:2023-02-12T08:59:11Z description:And this is my first blog post's description. Kinda great, huh? slug:{…} 2 properties _type:slug current:this-is-my-first-blog-post-can-t-you-believe-it title:This is my first blog post! Can't you believe it?? viewCount:1 1:{…} 9 properties _createdAt:2023-02-15T06:54:49Z _id:drafts.73a57d86-8f50-4917-9e0e-4013c2f0ea70 _rev:d365ed26-5987-483c-bcad-b0fce24ec2f8 _type:blogPost _updatedAt:2023-02-15T06:55:08Z description:Like things are fine, I don't know if this post was needed. slug:{…} 2 properties _type:slug current:this-is-my-second-post-whatever title:This is my second post. Whatever. viewCount:1000
We can now see the second blog post appear after the first. Let's clean up the query a bit:
*[_type == "blogPost"] { title, description, slug, viewCount}
Giving us this:
[…] 2 items 0:{…} 4 properties description:And this is my first blog post's description. Kinda great, huh? slug:{…} 2 properties _type:slug current:this-is-my-first-blog-post-can-t-you-believe-it title:This is my first blog post! Can't you believe it?? viewCount:1 1:{…} 4 properties description:Like things are fine, I don't know if this post was needed. slug:{…} 2 properties _type:slug current:this-is-my-second-post-whatever title:This is my second post. Whatever. viewCount:1000
It definitely looks a lot cleaner. But here's a question:
How do we only get the first post? Or only the second post?
Why slugs matter
We've talked a bit about how slugs are structured, but not much on why we care so much. When you go to a web page, you use the URL. And the URL gives us information on what page we want to go to, as well as the page we expect to receive.
This is an oversimplification of how all of that works just for the sake of this example.
A URL might be composed of just a domain name and a slug - meaning that we're relying on the slug to get our information. We'll talk more about the technical side of how we do that later. But for now, we can use GROQ to filter out blog posts that we aren't looking for.
Let's try this query and fetch the result:
*[_type == "blogPost" && slug == "this-is-my-first-blog-post-can-t-you-believe-it"][0] { title, description, slug, viewCount}
Wait, that's weird. Why didn't that work?
[] 0 items
If you haven't noticed, the actual "slug" part of our slug is stored in the current
field inside of the slug
field! So we actually want this query:
*[_type == "blogPost" && slug.current == "this-is-my-first-blog-post-can-t-you-believe-it"][0] { title, description, slug, viewCount}
Before we fetch the result, let's break down this new addition:
&&
: this means AND. In this query, we want to filter to make sure the document is both a blog post AND has the slug we wantslug.current
: this is the actual slug part of our document==
: this is the equality operator, which checks whether two things are equal to each other[0]
: GROQ follows zero-based numbering, meaning that rather than the first element starting at 1, it would start at 0. Since slugs are unique, we know we're only going to get one document, so we can add this at the end to only get the document. If we didn't add it, you would get an array with the single document inside.
In plain english, our query starts with: Fetch all documents that have the type "blogPost" AND have a slug of "this-is-my-first-blog-post-can-t-you-believe-it".
And if we fetch, we only get the blog post we were searching for:
{…} 4 properties description:And this is my first blog post's description. Kinda great, huh? slug:{…} 2 properties _type:slug current:this-is-my-first-blog-post-can-t-you-believe-it title:This is my first blog post! Can't you believe it?? viewCount:1
If we wanted the second blog post, we could just use the slug from the second blog post instead.
A question you might have is "Why do we need the slug if we know the first blog post comes before the second blog post?"
Since our headless CMS is separate from the website, the website has no idea where the blog post is! And even if it did, the first blog post might not always be ahead of the second blog post (or third, fourth, etc.)
When we talk about ordering with GROQ, you'll see that we can change the order in anyway we want, and it becomes pretty clear that we should use slugs as our identification of documents.
URL
We've talked a ton about Slug - let's get URL out of the way. It's pretty simple, so just add a field for a Social Media account to our Blog Post:
...,{ name: 'socialMediaLink', type: 'url', title: 'Social Media Link',},...
And, that's it. There's URI validation related to using http, https, and more: at default its set to allowing both. You can also add the required validation, like all the other fields.
Check out the Sanity Reference for URL for more.