A URL slug is the part of a URL path that names one page in words, usually derived from the page title. In https://example.com/blog/creme-brulee-a-30-minute-recipe/, the slug is creme-brulee-a-30-minute-recipe. Django’s documentation puts it briefly: “Slug is a newspaper term. A slug is a short label for something, containing only letters, numbers, underscores or hyphens” (SlugField). CMSs keep it in its own editable field: WordPress calls it the slug, Shopify calls it the handle.
Most advice about slugs repeats a few rules of thumb. This guide sticks to what can be checked: what Google’s documentation says, what WordPress and Shopify actually do with a title, how non-ASCII slugs end up in the URL, and how six slugify implementations disagree on the same input. The slugify outputs below were run on 2026-10-02 with the versions named in each section; the ZeroTool outputs come from the Slugify tool.
Where the Slug Sits in a URL
A slug is one path segment, or the last of several. RFC 3986 splits a URL into scheme, authority, path, query and fragment (§3); the slug lives in the path. Two common patterns:
https://example.com/2026/10/creme-brulee-a-30-minute-recipe/ WordPress, "Month and name" permalinks
https://shop.example.com/products/health-potion Shopify product handle
Slugs usually contain only lower-case ASCII letters, digits and hyphens. Those characters are all in RFC 3986’s unreserved set (A–Z a–z 0–9 - . _ ~, §2.3), so they never need percent-encoding. That is the practical reason slugs look the way they do: a slug made of unreserved characters reads the same in the address bar, in an email, in a log file and in a sitemap.
What Google’s URL Guidance Says
Google’s page URL structure best practices for Google Search has five recommendations that apply to slugs:
| Recommendation | Google’s example |
|---|---|
| Use descriptive URLs: “readable words rather than long ID numbers” | /wiki/Aviation instead of /index.php?topic=42&area=3a5ebc… |
| Use your audience’s language, transliterated if applicable | /lebensmittel/pfefferminz for German, /ペパーミント for Japanese |
| Use percent encoding as necessary; non-ASCII characters “should be percent encoded” in links | /gem%C3%BCse rather than /gemüse in href |
| Use hyphens to separate words, not underscores | /summer-clothing/ rather than /summer_clothing/ or /greendress |
| URLs are case sensitive | /APPLE and /apple are different URLs |
The hyphen rule comes with a reason: underscores are “commonly used for denoting concepts that should be kept together”, as in function names such as format_date. The case rule is why most slugifiers lowercase: if your server treats /Apple and /apple as the same page, Google still sees two URLs.
Just as useful is what the page does not say. It gives no length limit, says nothing about removing stop words such as “a” or “the”, and does not ask for keywords in the URL. Advice like “keep slugs under 60 characters” is a style choice, not a Google requirement.
How WordPress Builds a Slug
WordPress does not fix the slug while a post is a draft: wp_insert_post() leaves an empty post_name empty for draft, pending and auto-draft posts, and calls sanitize_title() on the title once the post is saved with another status, such as publish (post.php). sanitize_title() runs remove_accents() and then sanitize_title_with_dashes() (formatting.php). In WordPress 7.1.2 that function:
- Strips HTML tags and lowercases the text.
- Percent-encodes every non-ASCII character in lower-case hex, and stops at 200 encoded characters.
- Deletes curly quotes, ©, ®, °, … and similar signs; turns
/and.into-, and×intox. - Deletes everything except
a–z,0–9,%, spaces,_and-; turns spaces into hyphens; collapses repeated hyphens and trims them from both ends.
Running the real functions from 7.1.2 (with an en_US locale) on a few titles:
| Title | WordPress slug |
|---|---|
Rock & Roll | rock-roll |
John's Guide | johns-guide |
Node.js 20.0 | node-js-20-0 |
Fußgängerübergänge | fusgangerubergange |
東京 Travel Guide 2026 | %e6%9d%b1%e4%ba%ac-travel-guide-2026 |
Two results are worth knowing. ß becomes a single s because remove_accents() maps it that way unless the site locale is German; with de_DE the same title gives fussgaengeruebergaenge. And non-Latin text is kept, percent-encoded, which is exactly the form Google recommends for links. Because each Japanese or Chinese character takes 9 encoded characters, the 200-character cap cuts a long CJK title after about 22 characters.
If two posts get the same slug, wp_unique_post_slug() appends -2, -3 and so on. When you edit the slug of a published post, WordPress stores the old one as _wp_old_slug, and wp_old_slug_redirect() answers the old URL with a 301 whenever it would otherwise be a 404 (query.php).
How Shopify Builds a Handle
Shopify documents its handle rules in the Liquid basics: handles are always lower case; whitespace and special characters become a hyphen; runs of them become a single hyphen; leading ones are removed. Duplicates are “auto-incremented by one”, so a second product called Potion gets potion-1, where WordPress would use -2.
Two details matter later. “Changing the resource title doesn’t update the handle”, so a renamed product keeps its old URL until someone edits the handle. And when the handle is edited, the old path needs a redirect: Shopify’s URL redirects work “only from broken URLs”, meaning paths that now return 404, and an automation or app that changes handles through the Admin API has to create the redirect itself (Shopify Flow’s Create redirect URL action uses the urlRedirectCreate mutation).
Non-ASCII Slugs: Keep, Transliterate or Drop
Google explicitly allows slugs in the audience’s language, so a Japanese site can use /ペパーミント and a German one /gemüse. On the wire, and in any link you write, the path is percent-encoded UTF-8:
for (const word of ['ペパーミント', 'gemüse']) {
const encoded = encodeURIComponent(word);
console.log(word, word.length, encoded.length, encoded);
}
// ペパーミント 6 54 %E3%83%9A%E3%83%91%E3%83%BC%E3%83%9F%E3%83%B3%E3%83%88
// gemüse 6 11 gem%C3%BCse
Browsers show the decoded form in the address bar, but copying the URL into chat or a spreadsheet often pastes the encoded one. RFC 3986 treats upper- and lower-case hex as equivalent and asks producers to use upper case (§2.1); WordPress writes lower case, Google’s examples upper case, and both point to the same resource.
The alternatives each lose something:
- Transliterate. Accented Latin letters transliterate well (
crème→creme). Other scripts do not: python-slugify, which uses the text-unidecode tables, turns東京intodong-jing, the Mandarin reading, not the Japanese “Tokyo”. - Drop. ASCII-only slugifiers delete the characters.
東京 Travel Guide 2026becomestravel-guide-2026, and a title written entirely in Japanese becomes an empty string. Always check for an empty result and fall back to an ID or a hand-written slug. - Write the slug by hand. For CJK titles this is usually the best option: a short English or romanized slug chosen by an editor.
One Title, Six Slugifiers
The same titles through six implementations. WordPress is 7.1.2 with en_US; Django is 6.1.1 (django.utils.text.slugify); python-slugify is 9.1.2; slugify is the npm package 1.6.9 with { lower: true, strict: true }; @sindresorhus/slugify is 3.0.1.
| Title | ZeroTool | WordPress | Django | python-slugify | slugify (npm) | @sindresorhus/slugify |
|---|---|---|---|---|---|---|
Rock & Roll | rock-and-roll | rock-roll | rock-roll | rock-roll | rock-and-roll | rock-and-roll |
C++ vs C# | c-plus-plus-vs-c-hash | c-vs-c | c-vs-c | c-vs-c | c-vs-c | c-vs-c |
Fußgängerübergänge | fussgangerubergange | fusgangerubergange | fugangerubergange | fussgangerubergange | fussgangerubergange | fussgaengeruebergaenge |
John's Guide | johns-guide | johns-guide | johns-guide | john-s-guide | johns-guide | johns-guide |
Node.js 20.0 | node-js-20-0 | node-js-20-0 | nodejs-200 | node-js-20-0 | nodejs-200 | node-js-20-0 |
10% off @ Café | 10-percent-off-at-cafe | 10-off-cafe | 10-off-cafe | 10-off-cafe | 10percent-off-cafe | 10-off-cafe |
東京 Travel Guide 2026 | travel-guide-2026 | %e6%9d%b1%e4%ba%ac-travel-guide-2026 | travel-guide-2026 | dong-jing-travel-guide-2026 | travel-guide-2026 | travel-guide-2026 |
No row gives the same slug in all six. The disagreements fall into four groups:
- Symbols. Most implementations delete
&,+,#,%and@. ZeroTool and both npm packages write some of them as words; theslugifypackage even gluespercentto the number. - Dots and apostrophes. Django and
slugifywithstrictdelete dots, soNode.js 20.0becomesnodejs-200and the version number is unreadable; the other four turn the dot into a hyphen. Apostrophes split the other way: python-slugify treats a straight'as a separator (john-s-guide), the other five delete it (johns-guide). - German letters. Django’s ASCII mode drops
ßentirely (fugangerubergange); WordPress turns it into ones; @sindresorhus/slugify uses German rules forä ö üas well. - CJK. Kept and encoded, transliterated with Chinese readings, or deleted.
None of these is wrong in itself. What matters is consistency: if your site’s published URLs came from WordPress, a migration script that regenerates slugs with Django’s slugify will change node-js-20-0 into nodejs-200 and break every inbound link to that page. Generate slugs once, store them, and never recompute them from titles.
Changing a Slug After Publishing
Google’s advice for a URL that has to change is a permanent server-side redirect: “If you need to change the URL of a page as it is shown in search engine results, we recommend that you use a permanent server-side redirect whenever possible” (Redirects and Google Search). That means status 301 or 308 from the old slug to the new one.
- WordPress does this for posts automatically through
_wp_old_slug, as described above. - In Shopify, check under Content > Menus > URL redirects that the old handle has a redirect; it can only exist while the old path returns 404.
- Static sites need an explicit rule. On Cloudflare Pages, for example, a line such as
/blog/old-slug/ /blog/new-slug/ 301in_redirectsdoes it.
Keep the redirect indefinitely. Links in old emails, chat logs and other sites do not get updated.
A Slug Checklist
- Lower-case ASCII letters, digits and hyphens, unless you have chosen to use the audience’s language and accept percent-encoded links.
- Hyphens between words, as Google recommends, not underscores.
- Readable words that describe the page; length is your choice, not a Google rule.
- Check for an empty or near-empty result when titles may be written in non-Latin scripts.
- Decide how symbols such as
&,+and#should read (c-plus-plusorc) and use one slugifier for the whole site. - Store the slug; never regenerate it from the title.
- 301 or 308 from every old slug you retire.
To see what a title turns into before you publish, paste it into Slugify. To build an identifier such as user_login_count rather than a URL slug, use the Text Case Converter.