python-slugify + text-unidecode multilingual URL slugs: working versions (tested 2026-10-16)
Turn accented, Cyrillic and CJK titles into URL slugs with python-slugify, using text-unidecode for ASCII romanization or allow_unicode to keep scripts verbatim, then dedupe collisions.
passed6 passed · 0 failed2026-10-16 12:55 UTC- Status
- passed
- Last passed
- 2026-10-16 12:55 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions3
python-slugify@9.1.3text-unidecode@1.3.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38903329434
The code
Exactly the files that passed. Your agent can pull them with get_recipe("python-slugify-unidecode-titles") (install), or see them on GitHub.
requirements.txt
python-slugify==9.1.3 text-unidecode==1.3.0 pytest==9.1.1
slugs.py
from slugify import slugify
PAIR_REPLACEMENTS = [("C++", "cpp"), ("&", "and")]
def ascii_slug(title: str, max_length: int = 0, word_boundary: bool = False) -> str:
# allow_unicode defaults to False, so text-unidecode transliterates to ASCII
return slugify(
title,
max_length=max_length,
word_boundary=word_boundary,
replacements=PAIR_REPLACEMENTS,
)
def unicode_slug(title: str, lowercase: bool = True) -> str:
# keep CJK and other scripts verbatim instead of romanizing them
return slugify(title, allow_unicode=True, lowercase=lowercase)
def unique_slug(title: str, taken: set[str]) -> str:
base = ascii_slug(title) or "untitled"
if base not in taken:
return base
suffix = 2
while f"{base}-{suffix}" in taken:
suffix += 1
return f"{base}-{suffix}"
tests/test_slugs.py
from slugs import ascii_slug, unicode_slug, unique_slug
def test_ascii_transliterates_diacritics():
assert ascii_slug("Café Münster") == "cafe-munster"
assert ascii_slug("Straße") == "strasse"
assert ascii_slug("naïve façade") == "naive-facade"
def test_cjk_is_romanized_unless_allow_unicode():
assert ascii_slug("北京 大学") == "bei-jing-da-xue"
assert unicode_slug("北京 大学") == "北京-大学"
def test_replacements_run_before_symbols_are_stripped():
assert ascii_slug("C++ & Python") == "cpp-and-python"
def test_lowercase_false_keeps_only_allow_unicode_scripts():
assert unicode_slug("Zoë", lowercase=False) == "Zoë"
assert unicode_slug("Zoë") == "zoë"
# without allow_unicode the transliteration wins, so the umlaut is gone
assert ascii_slug("Zoë", max_length=0) == "zoe"
def test_max_length_respects_word_boundary():
assert ascii_slug("hello world", max_length=7) == "hello-w"
assert ascii_slug("hello world", max_length=7, word_boundary=True) == "hello"
def test_unique_slug_appends_counter_and_falls_back():
taken = {"hello-world", "hello-world-2"}
assert unique_slug("Hello World", taken) == "hello-world-3"
assert unique_slug("Fresh Title", taken) == "fresh-title"
assert unique_slug("!!!", taken) == "untitled"
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.