Paste "Crème Brûlée" into six different slugify libraries and you'll get six different strings back. Not one of them fully agrees with the others. Some keep the shape of the accent, some flatten it into a plain "e," and one German-focused converter starts speaking German whether you asked it to or not.
That last one throws people off constantly.
A slug if nobodys told you yet is the tail end of a URL. In textavia.com/blog/what-is-slugify the slug is what-is-slugify. The part after the slash the bit thats supposed to be readable by a human and safe for a server at the same time. The title is separate. Titles get rewritten, edited argued over in a Slack thread. The slug is supposed to sit there unchanged like a street address.
The word itself comes from newsroom slang. Back in the Linotype days a "slug" was a line of cast metal type. The working name a story carried through the print floor before it hit the page. Web developers borrowed the term decades later for the same idea: a short handle that identifies something.
More steps than you'd think
Six steps, roughly, though not every library bothers with all of them.
First, Unicode normalization, breaking é down into an e plus a little accent mark riding on top of it. Then transliteration, or stripping, depending on the tool. This step matters more than the rest, since it's where the output actually diverges. Spaces and punctuation get swapped for hyphens after that. Whatever's left that doesn't belong in a URL gets dropped, repeated hyphens get squashed down to one, and the whole thing gets lowercased.
Usually. Not always by default, annoyingly.
Run Crème Brûlée & Café through a typical pipeline and you land on creme-brulee-and-cafe. Simple enough looking at the result. Getting there is where six libraries start arguing.
The libraries don't agree, and it's kind of a mess
I tested five of them — npm's slugify, @sindresorhus/slugify, Django's built-in, python-slugify, and the one behind Textavia's own tool — against the same batch of tricky inputs. The differences aren't subtle.
| Input | npm slugify | @sindresorhus/slugify | Django | python-slugify | Textavia |
|---|---|---|---|---|---|
Crème Brûlée | Creme-Brulee | creme-brulee | creme-brulee | creme-brulee | creme-brulee |
Müller Straße | Muller-Strasse | mueller-strasse | muller-strae | muller-strasse | muller-strasse |
Fish & Chips | Fish-and-Chips | fish-and-chips | fish-chips | fish-chips | fish-and-chips |
Hello, World! 100% Free | Hello-World!-100percent-Free | hello-world-100-free | hello-world-100-free | hello-world-100-free | hello-world-100-percent-free |
C++ vs C# | C++-vs-C | c-vs-c | c-vs-c | c-vs-c | c-plus-plus-vs-c |
Санкт-Петербург | Sankt-Peterburg | sankt-peterburg | (empty) | sankt-peterburg | (empty) |
北京烤鸭 | (empty) | (empty) | (empty) | bei-jing-kao-ya | (empty) |
It's a Test | It's-a-Test | its-a-test | its-a-test | it-s-a-test | it-s-a-test |
2024/01/15 Report | 20240115-Report | 2024-01-15-report | 20240115-report | 2024-01-15-report | 2024-01-15-report |
Ærø Island | AEro-Island | a-ero-island | r-island | aero-island | aero-island |
A few of these are worth sitting with for a second.
npm's slugify does not lowercase anything unless you tell it to. First-timers hit this constantly, publish a URL with capital letters baked in, and don't notice for months. It also leaves stray punctuation like ! and + sitting in the output — you need the strict option, or you end up with something like hello-world!-100percent-free live on the internet.
Django, meanwhile, just deletes whatever it can't handle. No transliteration, no fallback, nothing. I could be wrong here, but that feels like the harshest approach of the bunch. The ß in Straße doesn't become ss. It vanishes. Ærø Island loses both the Æ and the ø, and what's left spells something else entirely. And a title written entirely in Cyrillic comes out as an empty string. Nothing. Django's own docs actually recommend turning on allow_unicode for anything outside plain Latin text, which keeps the original characters instead of eating them.
Ampersands go three separate directions depending on what you installed. & turns into and, or it just disappears, and npm's library will even localize the word if you ask — pass it a German locale and Fish & Chips becomes Fish-und-Chips, with 100% turning into 100prozent.
Only one of the five handles Chinese at all. python-slugify romanizes 北京烤鸭 into bei-jing-kao-ya using pinyin. Everything else in the test just gives up and returns nothing. I've seen this bite a food blog running entirely in Simplified Chinese titles once, every post slugifying to an empty string across two different platforms, before someone finally switched the backend to python-slugify specifically for the pinyin support.
Apostrophes split differently too. One library treats the apostrophe as junk to delete, another treats it as a separator and breaks the word in half. it's becomes its in one, it-s in the other. Neither approach is really wrong. They're just incompatible, which turns into a headache the day you migrate a site from one framework to another and every slug quietly changes shape underneath you.
What a URL technically allows
RFC 3986 is a lot more permissive than the conventions people actually follow. Technically, a path segment can include letters, numbers, hyphens, periods, underscores, tildes, and a small pile of extra characters like !, $, &, ', (, ), *, +, ,, ;, =, and @.
So yeah — hello-world!-100percent-free is a legal URL. Doesn't mean it's a good one. A + gets read as a literal space in query strings by some parsers. Semicolons and ampersands get mangled by software that wasn't built carefully. Periods collide with file extensions. And once a URL gets pasted into a text message, anything unusual has a decent shot of breaking the link, because whatever's rendering it has to guess where the address stops.
The set that survives basically every system is lowercase letters and digits. Hyphens too, but that's really the whole list.
Anything non-ASCII gets percent-encoded before it goes out over the wire, so müller shows up as m%C3%BCller in your server logs. Unreadable garbage the first time you see it turn up in an analytics export.
Hyphens beat underscores, and there's a dumb reason why
Google's own documentation recommends hyphens. So does basically every slugify library's default setting.
But nobody mentions the part where underscores disappear visually the moment a link gets underlined, which is most of the time, in most browsers. my_first_post and myfirstpost end up looking identical once the underline eats the gap. Hyphens survive that. Small thing.. Matters more than it should.
If you want the search side of this from someone who covers it properly, this walks through what Google actually says about URL structure:
A short pile of rules, none of them equally important
- Keep it short. Three to six words, ideally.
what-is-slugifydoes the job. A forty-character version with "complete-beginners-guide-for-2026" tacked on gets truncated in search results anyway. - Drop the filler words.
a,the,of,for— they rarely earn their spot in a URL. - Never date a slug unless the page is genuinely tied to that date. Otherwise you've committed to rewriting the URL every January, which nobody does, so it just sits there being wrong.
- Leading digits are fine in moderation, but a slug made up of only numbers tends to collide with ID-based routing in a lot of frameworks. Worth checking before it bites you.
- Pick a stance on stop words once and don't relitigate it site-wide.
- collisions happen. Two "Q3 Update" posts will both slugify to
q3-update, and most systems just tack on a-2, which works but looks lazy. - empty.
That last one deserves its own paragraph, actually, because it's easy to miss. A title written entirely in Cyrillic or Chinese can slugify down to literally nothing, an empty string, which breaks routing in ways that are annoying to debug late at night. Textavia's tool has a fallback option for exactly this: a stable generated string instead of a blank result, so the page doesn't just vanish because someone titled it in a script the pipeline doesn't recognize.
The rule that actually matters
A published slug functions like a contract the moment it goes live. It's in somebody's bookmarks. Pasted into a Slack thread from three months ago that nobody's ever going to circle back and fix. Sitting in a backlink on some other site you don't control, one you'll probably never even find. Change the slug, and every one of those breaks at once, silently, with no warning to anyone who relied on it.
Search engines treat the URL as the identity of the page, not the content sitting behind it. Change it without a redirect and a crawler sees a deleted page and a brand new one showing up out of nowhere, no history attached. Whatever ranking the old page built up just sits there, orphaned, until something points the crawler in the right direction.
If a slug absolutely has to change:
- Set up a permanent 301 redirect from the old address to the new one.
- Go fix the internal links so they point straight at the new URL instead of bouncing through the redirect.
- Update the canonical tag.
- Resubmit the sitemap.
- Leave the redirect there. Forever, basically. There's no safe day to remove it.
Redirects hold onto most of a page's standing. Not all of it, in my experience. Redirect chains add latency, and Google's said as much too. The cheapest redirect is always the one you never had to create in the first place.
Ways this goes wrong in practice
A CMS that regenerates the slug every time the title gets edited, well its a landmine. Someone fixes a typo in a headline six months after publish, hits save, and the live URL just changes underneath everyone without asking. I watched this happen to a client's recipe blog once — a redesign where a junior dev "cleaned up" every slug sitewide, swapped every underscore for a hyphen, no redirects set up at all. Traffic dropped by something like a third over the next quarter, and nobody connected the dots for weeks. Generate the slug once, at creation, and leave it alone forever.
Keyword-stuffing doesn't help either. best-cheap-free-online-slugify-tool-generator-2026 reads like spam and gets cut off in search results before anyone even reaches the "2026" part.
Mixed case is another one. /Blog/What-Is-Slugify and /blog/what-is-slugify are two completely different URLs on most servers, not the same page wearing different styling. Pick lowercase. Enforce it everywhere, including whatever script builds the sitemap.
(Also, this one's easy to overlook: don't assume your slugify function behaves the same way across versions. Transliteration tables get updated between releases sometimes, and if slugs are generated fresh every time a page loads instead of stored once at write time, a routine dependency upgrade can quietly rewrite your URLs without anyone touching a line of content.)
If you'd rather not write code
Textavia's tool runs in the browser, no upload, nothing logged. Paste a title, get a slug back instantly. (I still don't fully trust "runs entirely in the browser" claims until I check the network tab myself — old habit from a previous job.)
A few options change what comes out the other end. Preset mode defaults to RFC3986, which lowercases and strips anything not URL-safe. There's a "Pretty" mode that keeps capitals if you want them, and a Custom mode that hands you every knob individually. The separator defaults to a hyphen, and you'd only change it to an underscore if some legacy system genuinely requires it. Strict mode is off by default; turning it on drops anything outside letters, numbers, and the separator character.
Two things worth knowing about how Textavia handles edge cases. C++ becomes c-plus-plus instead of getting flattened down to a lonely c, and Ærø turns into aero rather than the mangled r you'd get from Django. Cyrillic and Chinese input still comes back empty, though. That's what the Base64 fallback option is there to catch.
In code, if that's more your speed
// npm slugify — set both options or you'll regret it
import slugify from 'slugify';
slugify('What Is Slugify?', { lower: true, strict: true });
// 'what-is-slugify'
# python-slugify handles way more scripts Django's built-in does
from slugify import slugify
slugify('What Is Slugify?')
# 'what-is-slugify'
# Django needs allow_unicode for anything non-Latin
from django.utils.text import slugify
slugify('Müller Straße') # 'muller-strae' (ß just gone)
slugify('Müller Straße', allow_unicode=True) # 'müller-straße'
"What Is Slugify?".parameterize
# => "what-is-slugify"
Str::slug('What Is Slugify?');
// "what-is-slugify"
Questions people actually ask
Is slugify even a real word? It's a made up verb from web development, borrowed from the old print sense of "slug." It's a function name in nearly every major framework at this point, which is basically how a made-up word becomes real vocabulary.
Does the slug affect search rankings? A little, indirectly, at least for most people's sites. Google's said URL wording is a minor factor at best. The bigger effect is on whether a human clicks — a readable slug in a search result tells someone what they're about to open before they open it.
Should the slug match the title exactly? No, and it shouldn't try to. Slugify the title, then cut it down. A title can run sixty characters. A slug should carry three or four words, tops.
What happens when two pages generate the same slug? Add a distinguishing word if you can manage it. q3-update-emea beats q3-update-2 for anyone actually reading the URL.
Can a slug have uppercase letters in it? Technically, yes. Practically, no — URL paths are case-sensitive on most servers, and mixed case invites duplicate links from anyone who retypes the address slightly wrong.
Is there a max length? Not officially, though total URL length should probably stay under 2,000 characters if older browsers matter to you. Most slugs that actually work stay under sixty.
Related tools
- Slugify
- URL Encoder — for percent-encoding query string characters
- Kebab Case
- Title Case — for the human-facing headline, the thing that sits above the slug
- Lowercase
Nothing you paste into any of these gets uploaded anywhere. Open the network tab in your browser while you use it, if you don't believe that — no request fires. I checked, mostly out of curiosity, and it's true.
