A title like "Crème Brûlée Café", "Привет мир" or "重庆火锅" has to become a URL slug somehow, and there are only three real choices: transliterate it into plain ASCII letters, keep the original characters and let them be percent-encoded, or write a slug in another language by hand. This guide compares them, checks what Google's documentation says, and walks through Latin accents, German umlauts, Cyrillic, Greek and Chinese pinyin with real output from our URL slug generator, plus where popular slugify functions silently lose letters.
Three ways to handle non-ASCII titles
| Approach | Example output | Strengths | Weaknesses |
|---|---|---|---|
| Transliterate to ASCII | creme-brulee-cafe, privet-mir, chong-qing-huo-guo | Works in every browser, app, CMS and server; easy to type | The romanized form must be readable for your audience; tone marks and some distinctions are lost |
| Keep Unicode | crème-brûlée-café, москва-гид, 重庆火锅 | Matches the words your audience reads and searches | Becomes long percent-encoded text wherever links are encoded; harder to type on other keyboards |
| Write or translate by hand | chongqing-hot-pot | Full control over the words | Manual work for every page; easy to become inconsistent |
Most international sites pick one approach per language and use it everywhere. The worst outcome is a mix, where some pages in the same section are romanized and others are not.
What Google says about non-English URLs
Google's URL structure guidelines say to use words in your audience's language in the URL, "and, if applicable, transliterated words". Its examples include a German path, /lebensmittel/pfefferminz, and a Japanese one, /ペパーミント. So both romanized and native-script URLs are acceptable to Google.
The same page adds two technical rules that matter here:
- Percent-encode non-ASCII characters in links. When you link to your pages, characters outside ASCII should be percent-encoded in the
hrefattribute. Google's example isgem%C3%BCserather thangemüse. - URLs are case sensitive. Google treats
/APPLEand/appleas different URLs, which is one more reason to lowercase consistently in every script that has case.
What percent-encoding does to a slug
A URL can only carry a limited set of ASCII characters, so every other character is sent as its UTF-8 bytes, each written as % plus two hexadecimal digits. Running the browser's own encodeURI and encodeURIComponent functions gives:
| Readable form | Encoded form |
|---|---|
https://example.com/café-москва | https://example.com/caf%C3%A9-%D0%BC%D0%BE%D1%81%D0%BA%D0%B2%D0%B0 |
重庆火锅 | %E9%87%8D%E5%BA%86%E7%81%AB%E9%94%85 |
Each accented Latin or Cyrillic letter takes two bytes (six characters encoded), and each Chinese character takes three bytes (nine characters). A four-character Chinese slug becomes 36 characters of encoded text. If your audience shares links in apps that show the encoded form, transliteration reads better. Paste any string into the URL encoder/decoder to see its bytes, or to decode a link back into readable text.
Latin accents and special letters
Most accented Latin letters are a base letter plus a mark, so removing the mark is safe: é → e, ñ → n, ç → c. A few letters have no mark to remove and need an explicit mapping: ß → ss, æ → ae, ø → o, ł → l, đ → d. Our engine handles both; the German umlaut option is covered in the next section. Here is real output next to the common one-line regex text.toLowerCase().replace(/[^a-z0-9]+/g, '-'):
| Title | Our default | Quick regex |
|---|---|---|
| Crème Brûlée Café | creme-brulee-cafe | cr-me-br-l-e-caf- |
| Straße über Ärger öffnen | strasse-uber-arger-offnen | stra-e-ber-rger-ffnen |
| Æbleskiver på Øresund | aebleskiver-pa-oresund | -bleskiver-p-resund |
| Łódź Kraków Gdańsk | lodz-krakow-gdansk | -d-krak-w-gda-sk |
| Đà Nẵng Phở Bò | da-nang-pho-bo | -n-ng-ph-b- |
The quick regex is fine for plain English but breaks words apart as soon as a title contains a single accent.
German, Danish and other language conventions
Some languages have their own fallback when special letters are unavailable. In German, ä, ö and ü are traditionally written ae, oe and ue, so mueller is closer to "Müller" than muller. WordPress follows the same idea: its remove_accents() function applies ä → ae, ö → oe, ü → ue and ß → ss only for German locales (de_DE, de_CH, de_AT), and å → aa, ø → oe for Danish. Without a locale, WordPress maps ß to a single s.
Our tool uses one general mapping for every language, plus a German umlauts switch. With it on, "Müller Größe Übersicht" becomes mueller-groesse-uebersicht instead of muller-grosse-ubersicht, and "Straße über Ärger öffnen" becomes strasse-ueber-aerger-oeffnen instead of strasse-uber-arger-offnen.
Danish å becomes a and ø becomes o in our mapping, as in aebleskiver-pa-oresund. If you need the Danish aa and oe forms, edit those slugs by hand.
Stop words are language-specific too. The stop word editor has Spanish, French and German presets that add to the English list: with the Spanish preset, "El Mejor Café de la Ciudad" becomes mejor-cafe-ciudad instead of el-mejor-cafe-de-la-ciudad; the French and German presets give meilleures-crepes-paris and tipps-garten.
Cyrillic and Greek
Cyrillic and Greek letters have no accent to strip, so they need a full letter table. Our engine uses one fixed, English-readable table for each script:
| Title | Slug |
|---|---|
| Привет мир | privet-mir |
| Щука и Ёж | shchuka-i-yozh |
| Юрий Хрущёв | yuriy-khrushchyov |
| Καλημέρα κόσμε | kalimera-kosme |
Several romanization systems exist for both scripts, and ours is one generic scheme, not a national standard. A name may therefore differ from its passport or map spelling. Edit brand, person and city slugs by hand, and spot-check Ukrainian, Serbian or Bulgarian text.
Chinese: pinyin slugs
Hanyu Pinyin is the international standard for romanizing Chinese (ISO 7098). Chinese characters have no letters to strip, so a plain slugify function either drops them or needs a pinyin dictionary. Our tools have an optional Chinese → pinyin setting that loads a dictionary only when your text contains Chinese:
| Title | Pinyin mode | Pinyin off | Keep Unicode |
|---|---|---|---|
| 重庆火锅 | chong-qing-huo-guo | (empty) | 重庆火锅 |
| 银行开户指南 | yin-hang-kai-hu-zhi-nan | (empty) | 银行开户指南 |
| 绿茶的好处 | lv-cha-de-hao-chu | (empty) | 绿茶的好处 |
| 北京 Travel Guide 2026 | bei-jing-travel-guide-2026 | travel-guide-2026 | 北京-travel-guide-2026 |
Details worth knowing:
- No tone marks. Tones are dropped because they are not ASCII.
- ü becomes v, as in
lv-cha. This is the convention of pinyin keyboards; some sites writeluorlyuinstead. - Polyphonic characters are read in context. 重 is chong in 重庆 and 行 is hang in 银行, because the dictionary looks at whole words. Rare names can still be misread.
- One hyphen per syllable. For word-level slugs such as
chongqing-hotpot, write them by hand. - Japanese and Korean are different. Japanese kanji get Chinese readings and kana are dropped:
東京 ラーメンbecomesdong-jing. Korean Hangul is dropped. For those languages, use Keep Unicode or write the slug yourself.
How popular slugify functions compare
Different platforms make different trade-offs, and some lose letters without any warning. Django's slugify() normalizes text with NFKD and then drops everything that is not ASCII. That removes accents correctly, but letters that do not decompose disappear completely. These are the results of Django's algorithm next to ours:
| Title | Django default | Django allow_unicode=True | Our default |
|---|---|---|---|
| Crème Brûlée Café | creme-brulee-cafe | crème-brûlée-café | creme-brulee-cafe |
| Straße über Ärger öffnen | strae-uber-arger-offnen | straße-über-ärger-öffnen | strasse-uber-arger-offnen |
| Æbleskiver på Øresund | bleskiver-pa-resund | æbleskiver-på-øresund | aebleskiver-pa-oresund |
| Привет мир | (empty) | привет-мир | privet-mir |
| 重庆火锅 | (empty) | 重庆火锅 | chong-qing-huo-guo (pinyin mode) |
WordPress's sanitize_title_with_dashes() "does not replace special accented characters" and percent-encodes them instead; when a post is saved, remove_accents() runs first with the locale rules above, so the result depends on the site language.
Code: transliterate accents in JavaScript and Python
If you need the Latin part of this in your own code, normalize to NFKD, remove the combining marks, and map the few letters that do not decompose first. These functions produce the same slugs as our engine for every Latin example in this guide, such as strasse-uber-arger-offnen and aebleskiver-pa-oresund:
const SPECIAL = { ß: 'ss', æ: 'ae', Æ: 'AE', ø: 'o', Ø: 'O', ł: 'l', Ł: 'L', đ: 'd', Đ: 'D', ı: 'i' };
function asciiSlug(text) {
return text
.replace(/[ßæÆøØłŁđĐı]/g, (c) => SPECIAL[c])
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
asciiSlug('Łódź Kraków Gdańsk'); // 'lodz-krakow-gdansk'import re
import unicodedata
SPECIAL = str.maketrans({"ß": "ss", "æ": "ae", "Æ": "AE", "ø": "o", "Ø": "O",
"ł": "l", "Ł": "L", "đ": "d", "Đ": "D", "ı": "i"})
def ascii_slug(text: str) -> str:
text = unicodedata.normalize("NFKD", text.translate(SPECIAL))
text = "".join(c for c in text if not unicodedata.combining(c)).lower()
return re.sub(r"[^a-z0-9]+", "-", text).strip("-")
ascii_slug("Straße über Ärger öffnen") # 'strasse-uber-arger-offnen'They do not handle Cyrillic, Greek or Chinese. For those scripts, use a full transliteration table or library, or our tools. Our guide to slugify regex and code snippets has complete functions for five languages.
Warning
Common mistakes
- Using an ASCII-only regex on international titles. As the table above shows,
[^a-z0-9]+turns "Crème" intocr-me. - Dropping letters silently. NFKD plus "ignore non-ASCII" loses ß, æ, ø, ł and đ, and empties Cyrillic and Chinese titles. Test your slug function with real titles from every language you publish.
- Publishing empty slugs. Our tools flag every title whose slug comes out empty; in your own code, add a fallback.
- Mixing schemes. Switching from
mullertomuelleror from Unicode to pinyin halfway through a site creates inconsistent URLs, and fixing it later means redirects. - Double encoding. Encoding an already encoded slug turns
%C3%A9into%25C3%25A9. Store readable slugs and encode once, when you build the link.
Multilingual slug checklist
[ ] One approach per language section: transliterate, keep Unicode, or translate
[ ] Latin accents removed (é -> e) and special letters mapped (ß -> ss, æ -> ae, ł -> l)
[ ] German content: decide ä -> a or ä -> ae, then use it on every page
[ ] Cyrillic and Greek romanized with one table; names checked by hand
[ ] Chinese: pinyin mode on, output scanned for misread names
[ ] Japanese and Korean: Keep Unicode or a hand-written slug
[ ] No empty slugs; no silently dropped letters
[ ] Non-ASCII links percent-encoded once in href
[ ] Language stop words removed where they do not change the meaning
[ ] Slugs lowercase and unique in their folderTry it in the tools
The URL slug generator has Smart transliteration, German umlauts, Chinese → pinyin and Keep Unicode switches, plus Spanish, French and German stop word presets. The bulk CSV slug generator applies them to a whole spreadsheet. To keep the romanized slugs short, follow our URL slug length guide. To strip accents from names or plain text without turning it into a slug, use the remove accents tool. For pinyin versus Chinese-character URLs in depth, read our Chinese URL slug guide.
Sources
- Google Search Central: URL structure best practices for Google Search
- WordPress Developer Resources: remove_accents() and sanitize_title_with_dashes()
- Django documentation: django.utils.text.slugify
- ISO: ISO 7098:2015, Romanization of Chinese
- MDN: encodeURI()
FAQ
Frequently asked questions
Should I transliterate URLs or keep native characters?
Both work for Google, which recommends words in your audience's language and accepts transliterated words. Transliterate when links are often shared or typed on other keyboards; keep native characters when your audience reads that script and your platform handles UTF-8 consistently.
Should ü become u or ue in a URL slug?
For German content, ue is the traditional spelling when umlauts are unavailable, so mueller is closer to Müller. For other languages, u is the usual choice. Pick one rule per site and keep it.
How do I make a slug from a Chinese title?
Turn on Chinese to pinyin in our tools: 重庆火锅 becomes chong-qing-huo-guo, without tone marks. You can also keep the Chinese characters with Keep Unicode, or write an English slug by hand.
Why does my slug come out empty?
The title contains only characters the function cannot romanize, such as Chinese with pinyin off, Japanese kana or Korean. Turn on pinyin for Chinese, switch to Keep Unicode, or write the slug by hand.
