Every blog, shop, and docs site does this quietly: you type a title, and somewhere a function turns it into the tidy string you see in the address bar. "The CSS Box Model, Explained!" becomes /blog/the-css-box-model-explained. That readable tail is called a slug, and building a good one is a deceptively nice little engineering problem. I built one that runs entirely in the browser, no libraries, and I want to walk through it because almost every step taught me something.
You can play with the live version here: https://dev48v.infy.uk/solve/day26-slug.html
What a slug actually has to be
Three things. Safe: only characters that survive a URL untouched, so no %20 or %C3%A9 gremlins. Legible: a human and a search engine can read the topic straight from the URL. Stable: once it's published, it's the permanent handle for that page.
The whole job is a short pipeline of string transforms, and the order matters more than any single step.
Stripping accents is one line (and it feels like cheating)
Most of the world's titles carry diacritics: café, naïve, Zürich, Beyoncé. You want café to become cafe, not some encoded mess. The trick is Unicode normalization form NFKD, which decomposes an accented letter into its base letter plus a separate combining mark. é literally becomes e followed by a "combining acute accent" codepoint.
Every combining mark lives in the range U+0300–U+036F, so one regex wipes them all:
function stripAccents(s) {
return s.normalize("NFKD").replace(/[̀-ͯ]/g, "");
}
// "Café Déjà Vu!" -> "Cafe Deja Vu!"
That single line is the part that makes a slugifier feel magic. Run it before you lowercase, while the letters are still their original characters.
The characters NFKD refuses to help with
NFKD only works on things that are "a base letter plus an accent." Plenty of characters aren't: the German ß, the ligatures æ and œ, the Nordic ø, and symbols like &. There's no accent to peel off, so they pass straight through.
For those you keep a small, explicit map and apply it first:
const MAP = { "ß":"ss", "æ":"ae", "œ":"oe", "ø":"o", "&":"and", "€":"eur" };
Keep this table intentional and short. Real transliteration of whole scripts (Chinese 北京 to beijing, Cyrillic Москва to moskva) needs big per-language dictionaries and is genuinely lossy, so that's where a dedicated library earns its place. My demo just drops what it can't map, so 東京 Tokyo 🚀 becomes tokyo.
Lowercase, then collapse everything else
URLs are treated as case-insensitive by users and many servers, so Hello-World and hello-world can become two URLs for one page: duplicate content, split ranking, doubled cache. Lowercasing removes the ambiguity.
Then you replace every run of non-alphanumerics with a single separator:
s = s.toLowerCase().replace(/[^a-z0-9]+/g, "-");
// "cafe deja vu!" -> "cafe-deja-vu-"
The + is doing quiet heavy lifting: it collapses "a & b" into a-b instead of a---b. Hyphen is the SEO-standard separator because search engines treat it as a word break; underscore is more of a filename and code-identifier thing.
Notice that trailing -? Titles that end in punctuation leave a dangling separator, so you trim both ends:
s = s.replace(/^-+|-+$/g, ""); // "cafe-deja-vu-" -> "cafe-deja-vu"
Two details that separate a toy from a real one
Length, cut on a word boundary. Long slugs are ugly and some systems cap URL length, so I allow a max. But a naive slice(0, max) guillotines words: the-quick-brownish truncates to the-quick-brow. The fix is to slice, then only back up to the last separator if you actually landed inside a word. If the cut fell on a boundary, keep it.
Uniqueness. Titles aren't unique. Two posts called "Hello World" both want hello-world, but a slug is usually a database key or a route. The standard answer is a numeric suffix: the first keeps hello-world, the next becomes hello-world-2, then -3. You check each candidate against the set of slugs already in use and bump until one is free.
function unique(base, taken) {
if (!taken.has(base)) return base;
let n = 2;
while (taken.has(`${base}-${n}`)) n++;
return `${base}-${n}`;
}
The one rule that isn't code
Once a slug is published, links point at it, search engines have indexed it, people have bookmarked it. If someone edits the title and you silently regenerate the slug, every one of those links 404s and you throw away the ranking you earned. So after first publish, freeze the slug, or if you must change it, keep the old one alive with a 301 redirect. Treat the slug as an identifier that merely started life derived from the title, not something that must forever mirror it.
That's the whole thing: normalize, transliterate, lowercase, collapse, trim, cap, dedupe. A pipeline of tiny transforms that quietly makes the web readable. The interactive build, with a live stage-by-stage trace, is here: https://dev48v.infy.uk/solve/day26-slug.html
Top comments (0)