ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.