StackPractices
beginner By Mathias Paulenko

How to Generate URL Slugs (Python, JS, Java)

How to generate clean, URL-friendly slugs from strings in Python, JavaScript, Java, and Go — with transliteration and uniqueness handling.

Topics: data

Overview

Titles, product names, and user-submitted text come loaded with spaces, accents, and punctuation — exactly the characters that mangle a URL (/posts/Café%20&%20Crème!). A slug converts that input into a safe, readable identifier like cafe-creme-brulee. The work involves transliterating non-ASCII characters, removing special symbols, collapsing whitespace, and — the part most examples skip — guaranteeing uniqueness when two titles produce the same slug.

When to Use

Use this resource when:

  • Converting article titles, product names, or user-generated content into permalink URLs
  • Building CMSs, blogs, e-commerce platforms, or any app with user-facing URLs
  • Normalizing filenames for uploaded assets to avoid encoding issues
  • Creating SEO-friendly paths for multi-language content
  • Slugifying user input — validate and sanitize it first, as covered in the input validation recipe

Skip slugs for identifiers that never appear in a URL. A UUID or numeric ID is simpler and avoids the uniqueness problem entirely.

Solution

The snippets are copy-paste ready; runnable versions for all four languages live in the companion repository.

Python

import re
import unicodedata

def generate_slug(text):
    # Normalize unicode and remove accents
    text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
    # Lowercase and replace non-alphanumeric with hyphens
    text = re.sub(r'[^\w\s-]', '', text.lower())
    # Collapse multiple hyphens/whitespace and trim
    text = re.sub(r'[-\s]+', '-', text).strip('-_')
    return text

print(generate_slug("Hello, World! 2024"))    # hello-world-2024
print(generate_slug("Café & Crème Brûlée"))   # cafe-creme-brulee

JavaScript

function generateSlug(text) {
    return text
        .normalize('NFKD')
        .replace(/[\u0300-\u036f]/g, '')  // Remove diacritics
        .toLowerCase()
        .trim()
        .replace(/[^\w\s-]/g, '')
        .replace(/[-\s]+/g, '-')
        .replace(/^-+|-+$/g, '');         // Trim edge hyphens
}

console.log(generateSlug("Hello, World! 2024"));    // hello-world-2024
console.log(generateSlug("Café & Crème Brûlée"));   // cafe-creme-brulee
// Using the popular slugify library
// npm install slugify
import slugify from 'slugify';

slugify('Hello, World! 2024');    // hello-world-2024
slugify('Café & Crème Brûlée');   // cafe-creme-brulee

Java

import java.text.Normalizer;
import java.util.Locale;

public class SlugGenerator {
    public static String generateSlug(String input) {
        String normalized = Normalizer.normalize(input, Normalizer.Form.NFKD)
            .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
        return normalized.toLowerCase(Locale.ROOT)
            .replaceAll("[^\\w\\s-]", "")
            .replaceAll("[-\\s]+", "-")
            .replaceAll("^-+|-+$", "");   // Trim edge hyphens
    }

    public static void main(String[] args) {
        System.out.println(generateSlug("Hello, World! 2024"));    // hello-world-2024
        System.out.println(generateSlug("Café & Crème Brûlée"));   // cafe-creme-brulee
    }
}

Explanation

The core algorithm is consistent across languages:

  1. Unicode normalization (NFKD) decomposes accented characters into base + combining mark, so é becomes e plus a removable diacritic
  2. Strip diacritics leaves plain ASCII letters
  3. Lowercasing gives you case-insensitive URLs
  4. Remove special characters except alphanumerics, hyphens, and spaces
  5. Collapse separators into single hyphens and trim the edges
Slug generation pipeline: a raw title goes through NFKD normalization, diacritic stripping, lowercasing, special character removal, separator collapsing and edge trimming to produce a clean URL slug

All the heavy lifting is regular expressions — see the regular expressions recipe if the patterns look unfamiliar. What comes out is lowercase ASCII delimited by hyphens, safe to drop into a URL path, a filename, or an ID column.

Handling Non-Latin Scripts

For languages like Chinese, Japanese, Arabic, or Russian, NFKD normalization alone falls short — those characters don’t decompose into ASCII at all, so the job calls for real transliteration:

from slugify import slugify

print(slugify('你好世界'))      # ni-hao-shi-jie
print(slugify('Привет мир'))  # privet-mir
import slugify from '@sindresorhus/slugify';

console.log(slugify('你好世界'));  // ni-hao-shi-jie

When no transliteration library is on the table, a random ID or a numeric suffix is a pragmatic fallback.

Python with Uniqueness Handling

import re
import unicodedata

def generate_slug(text, existing_slugs=None, max_length=100):
    text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
    text = re.sub(r'[^\w\s-]', '', text.lower())
    text = re.sub(r'[-\s]+', '-', text).strip('-_')
    text = text[:max_length].rstrip('-')

    if existing_slugs is None:
        return text

    base = text
    counter = 2
    while text in existing_slugs:
        suffix = f'-{counter}'
        text = base[:max_length - len(suffix)] + suffix
        counter += 1
    return text

existing = {'hello-world', 'hello-world-2'}
print(generate_slug('Hello World', existing))  # hello-world-3

JavaScript with Custom Options

function generateSlug(text, options = {}) {
  const {
    replacement = '-',
    remove = /[^\w\s-]/g,
    lower = true,
    strict = true,
  } = options;

  let result = text
    .normalize('NFKD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(remove, '')
    .trim()
    .replace(/[-\s]+/g, replacement);

  if (lower) result = result.toLowerCase();
  if (strict) result = result.replace(/[^a-z0-9-]/g, '');

  return result;
}

Go (Using golang.org/x/text)

package main

import (
    "fmt"
    "regexp"
    "strings"
    "unicode"

    "golang.org/x/text/unicode/norm"
)

func generateSlug(text string) string {
    t := norm.NFKD.String(text)

    var b strings.Builder
    for _, r := range t {
        if unicode.Is(unicode.Mn, r) {
            continue
        }
        b.WriteRune(r)
    }
    text = strings.ToLower(b.String())

    reg := regexp.MustCompile(`[^a-z0-9]+`)
    text = reg.ReplaceAllString(text, "-")

    return strings.Trim(text, "-")
}

func main() {
    fmt.Println(generateSlug("Hello, World! 2024"))   // hello-world-2024
    fmt.Println(generateSlug("Café & Crème Brûlée"))  // cafe-creme-brulee
}

Variants

TechnologyLibraryApproachNotes
Pythonpython-slugifyslugify()Handles unicode, supports language-specific rules
Pythonunicodedata + reManualNo dependencies, full control
JavaScriptslugifyslugify()Lightweight, supports custom replacements
JavaScriptManualnormalize() + regexZero dependencies
Javaslugify (Maven)SlugifySupports custom replacements and language rules
JavaNormalizer + regexManualBuilt-in JDK, no external deps
Gogolang.org/x/textnorm.NFKD + regexStdlib extended, no third-party deps

What Works

  • Always normalize Unicode first: strip diacritics after NFKD or é, ñ, and CJK input produces wrong output
  • Enforce a maximum length (~100 characters) to prevent URL bloat and oversized database columns; the truncate text recipe covers cutting at word boundaries
  • Check for uniqueness against existing slugs in your database; append -2, -3 if needed
  • Avoid leading/trailing hyphens: they look unprofessional and can break relative URL resolution
  • Use lowercase exclusively: URLs are case-sensitive on most Unix servers
  • Test with edge cases: empty strings, all-special-character input, very long strings, and mixed scripts
  • Apply locale-specific rules where they matter: German ä → ae and ß → ss, Turkish İ → i
  • Cache generated slugs if you compute them on every page load — NFKD normalization over a large catalog adds up

Common Mistakes

  • Stripping accents without NFKD normalization: accented characters either survive or get dropped entirely instead of becoming their base form
  • Letting reserved URL characters through: #, %, ?, &, /, and unencoded spaces break routing and query parsing
  • Not limiting slug length: extremely long slugs hurt readability and can exceed database column limits
  • Ignoring duplicate slugs: two articles with the same title will collide without a uniqueness strategy
  • Translating instead of transliterating: translating “hello” to Spanish isn’t the same as making it URL-safe
  • Not handling empty slugs: input made only of special characters produces an empty string — decide on a fallback (random ID, record ID, or an error)
  • Using _ instead of -: the underscore is a valid URL character, but Google treats hyphens as word separators, which is better for SEO
  • Not stripping consecutive separators: hello---world looks broken

Troubleshooting

  • Slug comes out empty: the input had no ASCII alphanumeric characters left after normalization. Check for empty output and fall back to a random suffix or the record ID.
  • Different titles produce the same slug: you’re probably stripping characters before normalizing, so accented input collapses to identical output. Run NFKD first, remove second.
  • Collisions under concurrent writes: two requests can both pass the uniqueness check before either one commits. Enforce a unique index and retry with a suffix, or wrap the check-and-insert in a transaction.
  • Changing a title silently changes the URL: regenerating the slug on every render makes old links rot. Store the slug once and only change it deliberately, with a 301 redirect.

Further Reading

Frequently Asked Questions

How do I handle completely non-Latin scripts like Chinese or Arabic?

For Chinese, use Pinyin romanization libraries (pypinyin in Python, pinyin in JS). Arabic, Persian, and Cyrillic go through transliteration libraries like unidecode or transliteration. When neither is an option, a random ID or numeric suffix works as a fallback.

Should slugs be unique globally or per user?

Scope the uniqueness to what the URL exposes. With /posts/:slug, slugs must be globally unique. With /:username/:slug, they only need to be unique per user. Either way, index the slug column for fast lookups.

Can I change a slug after publishing?

Changing slugs breaks existing links and bookmarks. When a change is unavoidable, 301-redirect the old slug to the new one and keep the old slug in a redirects table to preserve SEO value.

Should I store slugs in the database or generate them on the fly?

Store them. Generating slugs on the fly means a title change silently breaks URLs. Persist the slug as a column with a unique index, and when the title changes, decide whether to update the slug (with a redirect) or keep the old one.

How do I handle slug collisions at scale?

On a high-traffic site the naive -2, -3 suffix check races: in Postgres or MySQL, SELECT ... FOR UPDATE inside a transaction makes the check-and-insert atomic. Appending a short random suffix — 4 characters is usually enough — guarantees uniqueness without a lookup at all.

What is the ideal slug length?

Keep slugs between 3 and 75 characters. Search engines truncate URLs around 60-75 characters in results pages. Short slugs (under 3 characters) are ambiguous and may conflict with reserved paths. Truncate at the last complete word boundary to avoid splitting words.

How do I slugify content with emojis?

Strip emojis entirely — most URL parsers and browsers don't handle emoji URLs reliably. Python's re.sub(r'[\U0001F600-\U0001F64F]', '', text) or the emoji library removes them; JavaScript's Unicode-aware regex does the same with text.replace(/\p{Extended_Pictographic}/gu, '').

Are slugs case-sensitive?

Technically, URLs are case-sensitive per RFC 3986. Most servers normalize case in practice, though nothing forces them to — lowercasing everything sidesteps that ambiguity entirely. Some servers (nginx, Apache) can redirect uppercase URLs to lowercase for consistency.

What libraries handle transliteration for non-Latin scripts?

Python's python-slugify ships with transliteration tables covering most scripts out of the box, while unidecode handles many scripts but produces ASCII-only output. On the JavaScript side, @sindresorhus/slugify and the transliteration package handle Chinese, Cyrillic, and Arabic; in Java, junidecode gives you similar functionality. Test with your actual content — transliteration quality varies between libraries.

Should I use hyphens or underscores in slugs?

Use hyphens. Google treats hyphens as word separators, which improves SEO. The _ character is valid in URLs but Google doesn't recognize it as a separator, so hello_world reads as a single word.