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.
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:
- Unicode normalization (
NFKD) decomposes accented characters into base + combining mark, soébecomeseplus a removable diacritic - Strip diacritics leaves plain ASCII letters
- Lowercasing gives you case-insensitive URLs
- Remove special characters except alphanumerics, hyphens, and spaces
- Collapse separators into single hyphens and trim the edges
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
| Technology | Library | Approach | Notes |
|---|---|---|---|
| Python | python-slugify | slugify() | Handles unicode, supports language-specific rules |
| Python | unicodedata + re | Manual | No dependencies, full control |
| JavaScript | slugify | slugify() | Lightweight, supports custom replacements |
| JavaScript | Manual | normalize() + regex | Zero dependencies |
| Java | slugify (Maven) | Slugify | Supports custom replacements and language rules |
| Java | Normalizer + regex | Manual | Built-in JDK, no external deps |
| Go | golang.org/x/text | norm.NFKD + regex | Stdlib 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,-3if 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
ä→aeandß→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---worldlooks 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
- RFC 3986 — URI Generic Syntax: which characters are legal in a URL path segment.
- MDN —
String.prototype.normalize(): Unicode normalization forms in JavaScript. - python-slugify on PyPI: the de facto Python library, with transliteration via
text-unidecode. - Unicode Standard Annex #15: the specification behind NFKD decomposition.
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.
Related Resources
Parse CSV Files
How to parse CSV files in Python, Java, and JavaScript with practical code examples.
RecipeParse JSON
How to parse JSON strings into native data structures across multiple programming languages.
RecipeRegular Expressions
How to use regular expressions for pattern matching, validation, and text extraction across Python, JavaScript, and Java.
RecipeInput Validation: Schema-First Examples in 3 Languages
Validate user input at the API boundary with schema libraries. Pydantic, Zod, and Jakarta Validation examples with sanitization and structured errors.
RecipeTruncate Text
How to truncate text with ellipsis and word boundaries in Python, Java, and JavaScript.