Turn a title into a URL slug in seven formats, with diacritics folded by NFD, Greek and Cyrillic romanised, every character accounted for, and collisions reported
Initializing in your browser…
Percent-encode and decode against four character sets, build query strings, and take a URL apart
Generate QR codes and check each one scans by decoding it back before you download it
Encode and decode text, files and JSON as base64, base64url or hex, checked by decoding the result back
A content list with titles in four languages has to become URL paths, and the question is what happens to the ones that are not English.
Titles
Crème Brûlée: 10 Tips & Tricks (2024)! Привет мир Καλημέρα κόσμε 你好世界
Slugs, and what happened
Crème Brûlée: 10 Tips & Tricks (2024)!
creme-brulee-10-tips-and-tricks-2024
è and û folded by NFD, & spelled out as its own word
Привет мир
privet-mir (Cyrillic romanised, BGN/PCGN)
привет-мир (Unicode mode)
%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82-%D0%BC%D0%B8%D1%80
Καλημέρα κόσμε
kalimera-kosme (Greek romanised, ELOT 743)
καλημέρα-κόσμε (Unicode mode)
你好世界
(nothing) ASCII mode: every character was outside ASCII and
had no romanisation here. Switch to Unicode.
你好世界 (Unicode mode)
%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C
Every character is in the RFC 3986 unreserved set
encodeURIComponent leaves it unchanged
new URL() gives back the same stringThe first line is the easy case and every tool gets it right. The other three are where a single /[^\w\s-]/g fails: \w is [A-Za-z0-9_], so Cyrillic, Greek and Chinese are all deleted without a word, and a title in one of them becomes an empty slug. Here Cyrillic and Greek go through published romanisation schemes, so they come out readable in ASCII, and a script with no romanisation is named rather than silently dropped, with the Unicode alternative offered: 你好世界 is a perfectly valid URL path, it travels percent-encoded, and the address bar shows the readable form. The three lines at the bottom are checks rather than claims, run on every slug: the RFC 3986 unreserved set, encodeURIComponent, and the browser's own URL parser.
Turn a title into a slug, and see exactly what happened to every character rather than trusting that it worked. Diacritics are folded the standard way, Greek and Cyrillic are romanised from published schemes, symbols become words, and anything with no ASCII equivalent is either kept (a URL carries it perfectly well) or named and dropped, never deleted in silence. The result is checked against the RFC 3986 unreserved set and against the browser's own URL parser, and two titles that come out as the same slug are reported.
It goes wrong at `/[^\w\s-]/g`. In JavaScript `\w` is `[A-Za-z0-9_]` and nothing else, so that one expression deletes every character outside ASCII that a hand-written table happens not to cover, without a word. Measured on the version this replaced: eight of ten titles in non-Latin scripts came out as an empty string, and Γειά σου Κόσμε came out as "gammaepsilon-epsilon", which is worse than empty because it looks like an answer. Seventy-seven of the 190 letters in Latin-1 Supplement and Latin Extended-A were deleted outright, including every macron and every ogonek.
What happens here instead. Diacritics go through `normalize('NFD')` with the combining marks removed, which covers every composed Latin letter rather than the subset someone remembered to type; the project's test walks all 190 of them and compares each one against that fold. NFD alone is not enough, because a handful of letters carry no combining mark and so do not decompose at all: ø, đ, ł, þ, ð, ß, æ, œ, ħ, ı and their capitals, plus the compatibility ligatures. Those come from a table of 31 entries beside NFD, and the table is shown on the page rather than described.
Greek is romanised by ELOT 743, the scheme the Greek state uses on passports, and Cyrillic by BGN/PCGN: 25 and 43 letters, both listed on the page. Καλημέρα κόσμε becomes kalimera-kosme and Привет мир becomes privet-mir. One limitation is stated rather than hidden: the same Cyrillic letter is romanised differently in different languages and nothing can tell them apart from the letters alone, so Київ comes out kiyiv in the Russian reading rather than the familiar Kyiv, and the page says so when a Ukrainian-only letter appears.
Chinese, Japanese, Korean, Arabic, Hebrew, Thai and Devanagari have no romanisation here, because a correct one needs a dictionary rather than a table. In ASCII mode those characters are named and dropped, with an error saying what to do about it. In Unicode mode they are kept: 你好世界 stays 你好世界, which is a perfectly valid URL path, travels percent-encoded, and shows in the address bar in its readable form. The page shows the encoded form beside it so nothing is a surprise.
Twenty-one symbols become words, each surrounded by spaces so it gets its own place: C++ becomes c-plus-plus rather than cplusplus, and me@you becomes me-at-you rather than meatyou. The 88-word English stop list can be applied, and is shown in full; if every word in a title is on it the words are kept rather than leaving nothing, and the page says why. A length cap cuts at a word boundary in every format, including camelCase and PascalCase, where the boundary is the change of case.
Three checks run on the result and are shown: whether every character is in the RFC 3986 unreserved set, whether `encodeURIComponent` leaves it alone, and what `new URL()` gives back when it is used as a path segment. Batch mode takes one title per line, reports every group of titles that produce the same slug, and adds a -2, -3 column, which is what a CMS does. Compare mode shows all seven formats at once.
The title stays in the page. Settings go into the share link so a colleague gets the same configuration; the text does not.
Καλημέρα κόσμε becomes kalimera-kosme by ELOT 743, where a hand table covering only alpha and beta produced "gammaepsilon-epsilon".
"Straße, Łódź, Þórshöfn, Đà Nẵng" becomes strasse-lodz-thorshofn-da-nang: four letters that NFD cannot fold and two that it can, all handled.
A title in any language becomes a path segment that a router will accept, and a title that collides with an existing one is caught before it is published.
snake_case and kebab-case with a length cap that never leaves half a word, which is what breaks a name when it is cut at a fixed offset.
Batch mode over a title export shows every collision at once, which is the thing that silently loses a page during a migration.
camelCase, PascalCase and CONSTANT_CASE from the same title, side by side in Compare.
Because of a single regular expression. `/[^\w\s-]/g` keeps only `[A-Za-z0-9_]`, whitespace and hyphens, so anything outside ASCII that a hand-written table did not cover was deleted. Here Cyrillic and Greek are romanised from published schemes, and a script with no romanisation is either kept or named and dropped with an explanation.
Yes. A path segment is allowed to contain any character; the ones outside the unreserved set travel percent-encoded, which every browser and every mainstream server handles, and the address bar shows the readable form. The page shows the encoded form so you know what a raw log will contain. If a system in your stack cannot cope, use the ASCII mode.
Because that is what it is. The German sharp s is a double s, and NFD cannot help because it carries no combining mark: it is its own letter. The same applies to æ, œ, þ, ð, ø, đ, ł and a few others, which is why there is a table of 31 letters beside the NFD fold.
Because the Cyrillic table is BGN/PCGN in its Russian reading, where и is i. Ukrainian romanises the same letter as y. Nothing can tell which language a string is from the letters alone, so the page says so when a letter only Ukrainian and Belarusian use appears, rather than guessing.
Two titles that produce the same slug will fight over one URL, and that is invisible one title at a time. "Hello World", "hello world", "Hello, World!" and "Héllo Wörld" are all hello-world. Batch mode groups them and adds a numbered column, which is what a CMS does when it notices.
Because a spelled-out symbol is a word. Each of the 21 symbols is surrounded by spaces before the words are joined, so it takes its own place in the slug. Turn the option off and a symbol is simply a word boundary, which makes C++ into c.
Only when a single word is longer than the cap, and then there is nowhere to back up to. Otherwise it drops whole words, in every format: camelCase and PascalCase have no separator, so the word boundary is the change of case and that is what it backs up to.
Conversions run on your device in JavaScript. The values you enter are never sent over the network.