Convert a String to a URL Slug
Slugifying turns human titles like `"Côte d'Ivoire, 2025!"` into URL-safe segments like `'cote-d-ivoire-2025'`. Done well, it covers Unicode normalization, accent stripping, and edge punctuation; done poorly, it ships duplicate slugs or 404s. This snippet starts with a regex-only baseline, layers in `normalize('NFD')` for diacritics, and ends with a hardened version that collapses dashes and bounds the length.
626 views
18
function slugify(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
console.log(slugify('Hello World!')); // hello-world
console.log(slugify('Top 10 JS Tips')); // top-10-js-tips
console.log(slugify(' spaces & symbols ')); // spaces-symbolsThe simplest correct slugify lowercases the input, replaces every run of non-alphanumeric characters with a single dash, then trims leading and trailing dashes. The character class [a-z0-9] is intentionally narrow so anything we did not anticipate (punctuation, accented letters, emoji) collapses to a separator instead of leaking into the URL. The + quantifier on the negated class is the trick that prevents 'a b' from producing 'a--b'. This baseline is fine for ASCII-only content like English headings or programming keywords.
function slugifyUnicode(str) {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // strip combining marks
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
console.log(slugifyUnicode("Cote d'Ivoire")); // cote-d-ivoire
console.log(slugifyUnicode('Naïve café résumé')); // naive-cafe-resume
console.log(slugifyUnicode('München, Düsseldorf')); // munchen-dusseldorfReal-world titles include accented Latin (é, ñ, ü), and the ASCII baseline turns them all into dashes, so 'café' becomes 'caf'. The fix is normalize('NFD'), which decomposes each accented letter into a base character plus a combining diacritic, followed by a regex strip of the combining-mark range \u0300-\u036f. This converts 'é' to 'e' instead of dropping it entirely, which keeps the slug readable and preserves search relevance. Note that this only handles diacritics, not script transliteration: Cyrillic and CJK still collapse to dashes, which usually means you should slug the ID, not the title, for those locales.
function slugifySafe(str, { maxLength = 80 } = {}) {
if (typeof str !== 'string') return '';
const slug = str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, maxLength)
.replace(/-+$/, '');
return slug;
}
console.log(slugifySafe("Côte d'Ivoire, 2025!")); // cote-d-ivoire-2025
console.log(slugifySafe(' ---weird---input--- ')); // weird-input
console.log(slugifySafe('A very long title that will be truncated by the maxLength bound to keep URLs short', { maxLength: 30 }));
// a-very-long-title-that-will-beA production slug helper has to defend against two more issues: long titles that produce unwieldy URLs, and the trailing dash that appears if the slice happens to land mid-word. The optional maxLength cap (default 80, matching common CMS limits) plus a final replace(/-+$/, '') strips any orphan trailing dash created by truncation. The added replace(/-{2,}/g, '-') is a belt-and-braces pass: even though the previous regex collapses most runs, the order of normalize plus strip plus replace can still produce double dashes for some inputs. Use this version anywhere slugs land in routes or filenames.
