Turn a title into a URL slug
Converts any title into a lowercase, hyphenated URL slug, stripping accented characters via Unicode normalization along the way.
Last updated
function slugify(text) {
return text
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
// slugify("Café Déjà Vu!") -> "cafe-deja-vu"How it works
normalize("NFD") decomposes each accented character into a base letter plus a separate combining-accent codepoint (é becomes e + ´), which is what lets the next line strip every accent with one regex: \u0300-\u036f is exactly the Unicode block those combining marks live in. Doing it this way avoids hand-listing every accented letter you might encounter.
The last two replace calls do the actual slugging: any run of characters that isn't a-z or 0-9 collapses to a single dash, then leading/trailing dashes are trimmed — so "100% Done -- Now" becomes "100-done-now" in one pass rather than three separate cleanup steps.
Edge cases to know
- →This only strips Latin diacritics. Non-Latin scripts (Chinese, Arabic, Cyrillic) have no ASCII equivalent to fall back to, so those characters are simply dropped — you'll need transliteration for those, not normalization.
- →Collisions aren't handled: "My Post" and "My Post!" both slugify to "my-post", so uniqueness (append an id or counter) is the caller's job.
- →An all-symbols input (e.g. "???") slugifies to an empty string — worth a fallback (like a generated id) if that's a real input you expect.
Part by part
- normalize("NFD")
- Splits accented characters into a base letter plus a combining mark, so é becomes e followed by an accent that can be removed separately.
- [\u0300-\u036f]
- The combining diacritical marks block. Removing this range after NFD is what turns café into cafe rather than caf.
- [^a-z0-9]+
- Any run of characters that is not a lowercase letter or digit becomes a single hyphen. The plus is what collapses " - " into one separator rather than three.
- ^-+|-+$
- Strips leading and trailing hyphens, which is what the previous step leaves behind when the title starts or ends with punctuation.
Common mistakes
text.toLowerCase().replace(/ /g, "-")Handles spaces only, so punctuation, accents and repeated spaces all survive into the URL. The result looks right for a simple title and breaks on the first apostrophe.
text.normalize("NFC").replace(/[\u0300-\u036f]/g, "")NFC composes characters rather than decomposing them, so é stays a single code point and the removal matches nothing. The two forms are opposites and only NFD works here.
Omitting the trailing .replace(/^-+|-+$/g, "")A title ending in a question mark produces a slug ending in a hyphen, which is ugly and creates two URLs for the same content if anything later trims it.
Questions
What happens to non-Latin scripts?
They are removed entirely, so a title in Greek, Cyrillic or Chinese produces an empty slug. If you support those languages, transliterate first or fall back to an id rather than shipping an empty string.
How do I handle German umlauts properly?
This turns ü into u, whereas German convention expands it to ue. Add explicit replacements before normalising if that matters — no general algorithm gets every language's convention right.
Should slugs be unique?
Two different titles can produce the same slug, so the database needs a uniqueness constraint and a strategy for collisions — usually appending a counter or a short id.
What about very long titles?
Nothing here limits length. Truncate at a word boundary rather than mid-word, then strip any trailing hyphen again, or you reintroduce exactly what the last step removed.