python-slugify converts text to URL-safe slugs with Unicode transliteration. pip install python-slugify. Basic: from slugify import slugify; slugify("Hello World!") → “hello-world”. Unicode: slugify("Héllo Wörld") → “hello-world”. CJK: slugify("今日は") → transliterated pinyin. slugify("München") → “munchen”. Separator: slugify("Hello World", separator="_") → “hello_world”. separator="" → “helloworld”. Max length: slugify("Long title here...", max_length=20) → truncated. max_length=50, word_boundary=True — truncate at word boundary. Lowercase: slugify("HELLO WORLD") → “hello-world” (lowercase by default). lowercase=False → preserve case. Stopwords: slugify("The quick brown fox", stopwords=["the","a","an"]) → “quick-brown-fox”. Regex: slugify("a!@#$b", regex_pattern=r"[^a-z0-9]+") → “a-b”. allow_unicode: slugify("münchen", allow_unicode=True) → “münchen” (preserves umlauts). Replacements: slugify("C++ language", replacements=[["C++","cpp"],["#","sharp"]]) → “cpp-language”. Pre/post: slugify(text, pre_process_list=[("&","and")], post_process_text=str.upper). Unique: append -2 -3 for duplicates. Django: from django.utils.text import slugify (built-in). AutoSlugField in django-autoslug. Jinja2: env.filters["slugify"] = slugify. Claude Code generates python-slugify URL builders, title normalizers, and slug uniqueness pipelines.
CLAUDE.md for python-slugify
## python-slugify Stack
- Version: python-slugify >= 8.0 | pip install python-slugify
- Basic: slugify("Hello World!") → "hello-world" — strips punctuation, lowercases
- Unicode: slugify("München") → "munchen" | allow_unicode=True to keep ä/ü/ö
- Options: separator="_" | max_length=50, word_boundary=True | lowercase=False
- Stopwords: slugify(title, stopwords=["the","a","an","of"]) — removes function words
- Replacements: replacements=[["C++","cpp"],["&","and"]] — apply before slugifying
- Jinja2: env.filters["slugify"] = slugify | {{ title | slugify }}
python-slugify URL Slug Pipeline
# app/slugs.py — python-slugify generation, uniqueness, and URL builders
from __future__ import annotations
import re
import unicodedata
from typing import Callable
from slugify import slugify
# ─────────────────────────────────────────────────────────────────────────────
# 1. Core slug helpers
# ─────────────────────────────────────────────────────────────────────────────
# Standard stopwords for title slugs
_STOPWORDS = [
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to",
"for", "of", "with", "by", "from", "as", "is", "are", "was", "were",
]
# Common symbol replacements before slugification
_REPLACEMENTS = [
["&", "and"],
["+", "plus"],
["@", "at"],
["#", "number"],
["C++", "cpp"],
["C#", "csharp"],
["f#", "fsharp"],
]
def make_slug(
text: str,
max_length: int = 80,
separator: str = "-",
lowercase: bool = True,
word_boundary: bool = True,
stopwords: list[str] | None = None,
allow_unicode: bool = False,
replacements: list[list[str]] | None = None,
) -> str:
"""
Generate a URL-safe slug from arbitrary text.
max_length + word_boundary=True truncates at a whole word.
allow_unicode=True keeps non-ASCII characters (é, ü, 日) in the slug.
replacements: pre-process substitutions applied before transliteration.
"""
return slugify(
text,
separator=separator,
max_length=max_length,
word_boundary=word_boundary,
lowercase=lowercase,
stopwords=stopwords or [],
allow_unicode=allow_unicode,
replacements=replacements or _REPLACEMENTS,
)
def title_slug(title: str, max_length: int = 60) -> str:
"""
Blog/post slug: strip stopwords, limit to ~60 chars at a word boundary.
"The Quick Brown Fox Jumps Over The Lazy Dog"
→ "quick-brown-fox-jumps-over-lazy-dog"
"""
return slugify(
title,
max_length=max_length,
word_boundary=True,
stopwords=_STOPWORDS,
replacements=_REPLACEMENTS,
)
def filename_slug(text: str) -> str:
"""
Safe filename base (no extension): replaces spaces and special chars.
"My Report (2024) — Final" → "my-report-2024-final"
"""
return slugify(text, separator="-", max_length=120, word_boundary=True)
def username_slug(text: str) -> str:
"""
Username-safe slug: lowercase, no separators, alphanumeric only + dash.
"John Doe" → "john-doe"
"""
return slugify(text, separator="-", max_length=40, regex_pattern=r"[^a-z0-9\-]+")
def tag_slug(tag: str) -> str:
"""Normalize a tag: "Python 3.x" → "python-3-x"."""
return slugify(tag, max_length=50, word_boundary=False)
def category_slug(category: str) -> str:
"""Category slug: consistent lowercase with hyphens."""
return slugify(category, max_length=60, word_boundary=True)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Unique slug generator
# ─────────────────────────────────────────────────────────────────────────────
def unique_slug(
text: str,
existing: set[str] | list[str],
max_length: int = 60,
) -> str:
"""
Generate a unique slug by appending -2, -3, ... until unique.
existing: already-used slugs (from database or in-memory set).
"python-guide" (exists) → "python-guide-2"
"python-guide-2" (exists) → "python-guide-3"
"""
existing_set = set(existing)
base = slugify(text, max_length=max_length - 4, word_boundary=True)
slug = base
counter = 2
while slug in existing_set:
slug = f"{base}-{counter}"
counter += 1
return slug
class SlugRegistry:
"""
In-memory unique slug registry.
Use in bulk-import or export pipelines where you want unique slugs
without hitting the database for every item.
"""
def __init__(self) -> None:
self._used: set[str] = set()
def register(self, text: str, max_length: int = 60) -> str:
"""Register text and return a unique slug."""
slug = unique_slug(text, self._used, max_length=max_length)
self._used.add(slug)
return slug
def all(self) -> set[str]:
return set(self._used)
# ─────────────────────────────────────────────────────────────────────────────
# 3. URL builders
# ─────────────────────────────────────────────────────────────────────────────
def build_post_url(title: str, post_id: int | str, base: str = "") -> str:
"""
Build a blog post URL: /posts/{id}-{slug}
e.g. /posts/42-quick-brown-fox
"""
slug = title_slug(title)
return f"{base}/posts/{post_id}-{slug}"
def build_product_url(name: str, sku: str, base: str = "") -> str:
"""Build a product URL: /products/{slug}-{sku}"""
slug = make_slug(name, max_length=60)
return f"{base}/products/{slug}-{sku.lower()}"
def build_breadcrumb_path(*parts: str) -> str:
"""
Build a URL path from a sequence of strings.
build_breadcrumb_path("Blog", "Technology", "Python") → "/blog/technology/python"
"""
slugs = [slugify(p, max_length=40) for p in parts if p.strip()]
return "/" + "/".join(slugs)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Slug validation
# ─────────────────────────────────────────────────────────────────────────────
_VALID_SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
def is_valid_slug(slug: str) -> bool:
"""Return True if the string matches slug format (lowercase, hyphens only)."""
return bool(_VALID_SLUG_RE.match(slug))
def normalize_incoming_slug(slug: str) -> str:
"""
Normalize a user-submitted slug: re-slugify to strip any invalid characters.
Useful when accepting slugs in API inputs.
"""
return make_slug(slug)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Jinja2 filter registration
# ─────────────────────────────────────────────────────────────────────────────
def register_slug_filters(env) -> None:
"""
Register slugify as Jinja2 template filters.
Usage:
{{ post.title | slugify }}
{{ tag.name | tag_slug }}
{{ post.id ~ "-" ~ post.title | slugify }}
"""
env.filters["slugify"] = title_slug
env.filters["tag_slug"] = tag_slug
env.filters["cat_slug"] = category_slug
env.filters["filename"] = filename_slug
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Basic slugs ===")
samples = [
"Hello World!",
"The Quick Brown Fox Jumps Over The Lazy Dog",
"C++ Programming Language",
"München & Beyond",
"100% Pure Python",
" extra spaces ",
"UPPER CASE TITLE",
"Héllo Wörld — 2024 Edition",
"python-already-a-slug",
]
for s in samples:
print(f" {s!r:45} → {make_slug(s)!r}")
print("\n=== Title slugs (with stopwords) ===")
titles = [
"The Art of Python Programming",
"A Guide to Unit Testing",
"How to Build a REST API with FastAPI",
"Introduction to Machine Learning",
]
for t in titles:
print(f" {t!r:50} → {title_slug(t)!r}")
print("\n=== Unicode ===")
for text, allow_uni in [
("München", False),
("München", True),
("日本語テスト", False),
("hello-世界", False),
("Привет мир", False),
]:
print(f" {text!r:20} unicode={allow_uni} → {make_slug(text, allow_unicode=allow_uni)!r}")
print("\n=== Unique slugs ===")
registry = SlugRegistry()
posts = [
"Python Guide",
"Python Guide", # duplicate
"Python Guide", # third
"JavaScript Tips",
"Python Guide", # fourth
]
for title in posts:
slug = registry.register(title)
print(f" {title!r:25} → {slug!r}")
print("\n=== URL builders ===")
print(f" Post: {build_post_url('Hello World', 42)}")
print(f" Product: {build_product_url('Blue Running Shoes', 'SKU-1234')}")
print(f" Path: {build_breadcrumb_path('Blog', 'Technology', 'Python Tips')}")
For the django.utils.text.slugify alternative — Django’s built-in slugify() works for English text, handles basic Unicode ASCII conversion with allow_unicode=True, but lacks stopword removal, custom replacements (C++ → cpp), word-boundary truncation, and separator customization; python-slugify wraps Unidecode for better transliteration of CJK/Arabic/Cyrillic characters and exposes a richer options API for all these use cases. For the re.sub(r"[^a-z0-9]+", "-", text.lower()) approach — a raw regex can build slugs but fails on accented characters (é → not stripped, not transliterated), CJK (becomes ”---”), and edge cases (double hyphens, leading/trailing hyphens); python-slugify handles all of these correctly using Unicode normalization and the optional Unidecode transliteration. The Claude Skills 360 bundle includes python-slugify skill sets covering slugify() with separator/max_length/word_boundary/lowercase, allow_unicode=True for preserving ä/ü/ö, stopwords for removing function words, replacements for symbol substitution (C++ → cpp), title_slug() with stopwords, filename_slug(), username_slug(), tag_slug(), unique_slug() with counter suffix, SlugRegistry in-memory registry, build_post_url/build_product_url/build_breadcrumb_path URL builders, is_valid_slug() validation, and Jinja2 filter registration. Start with the free tier to try URL slug generation code generation.