A slugification library for Gleam that converts text into URL-friendly slugs.
▶ Try it live in the playground — every slug on that page is generated by this exact library, compiled from Gleam to JavaScript and running in your browser. Tweak the configuration toggles and copy the generated Gleam code straight into your project.
gleam add glugifyimport glugify
// Simple usage - always returns a string
glugify.slugify("Hello, World!")
// -> "hello-world"
// Error-aware usage - returns Result
glugify.try_slugify("My Blog Post Title!")
// -> Ok("my-blog-post-title")Zero-configuration slugification that always returns a string:
import glugify
glugify.slugify("My awesome blog post!")
// -> "my-awesome-blog-post"
glugify.slugify("Café & Restaurant")
// -> "cafe-and-restaurant"Returns Result(String, SlugifyError) for explicit error handling:
import glugify
case glugify.try_slugify("") {
Ok(slug) -> "Generated slug: " <> slug
Error(error) -> "Failed to generate slug"
}Full control with custom configuration:
import glugify
import glugify/config
let custom_config = config.default()
|> config.with_separator("_")
|> config.with_max_length(20)
|> config.with_word_boundary(True)
glugify.slugify_with("A Very Long Title That Needs Truncation", custom_config)
// -> Ok("a_very_long_title")import glugify/config
import glugify/locale
config.default()
|> config.with_separator("_") // Default: "-"
|> config.with_lowercase(False) // Default: True
|> config.with_max_length(50) // Default: None
|> config.with_word_boundary(True) // Default: False
|> config.with_transliterate(False) // Default: True
|> config.with_allow_unicode(True) // Default: False
|> config.with_custom_replacements([ // Default: []
#("&", " and "),
#("@", " at ")
])
|> config.with_stop_words(["the", "a"]) // Default: []
|> config.with_preserve_leading_underscore(True) // Default: False
|> config.with_preserve_trailing_dash(True) // Default: False
|> config.with_locale(locale.German) // Default: locale.Default
|> config.with_decamelize(True) // Default: False
|> config.with_decode_html_entities(True) // Default: False
|> config.with_ignore(["#"]) // Default: []There is also an SEO-tuned preset (60-character limit, word-boundary truncation, per search engine URL guidance):
glugify.slugify_with(long_title, config.seo_preset())When slugifying many titles (tables of contents, CMS imports, static sites), use glugify/slugger to guarantee uniqueness. The state is an immutable value, so it threads naturally through folds and behaves identically on both targets:
import glugify/slugger
let s = slugger.new()
let #(s, a) = slugger.slug(s, "Hello World")
let #(s, b) = slugger.slug(s, "Hello World")
let #(_, c) = slugger.slug(s, "Hello World")
// a -> "hello-world"
// b -> "hello-world-1"
// c -> "hello-world-2"Suffixed slugs never collide with slugs from genuinely suffixed input: "foo", "foo", "foo-1" yields foo, foo-1, foo-1-1. Use slugger.slug_with to combine uniqueness with a custom Config.
For markdown tooling, static site generators and tables of contents that must agree with GitHub about which #id a heading gets, glugify/anchor implements the exact github-slugger algorithm — validated against the github-slugger test fixtures on both targets:
import glugify/anchor
anchor.to_anchor("Hello, _World_!")
// -> "hello-_world_"
let a = anchor.new()
let #(a, first) = anchor.anchor(a, "Introduction")
let #(_, second) = anchor.anchor(a, "Introduction")
// first -> "introduction"
// second -> "introduction-1"This is deliberately a different algorithm from glugify.slugify: GitHub keeps underscores and every Unicode letter (no transliteration), turns each space into exactly one hyphen without collapsing, and does not trim. Use anchor.to_anchor_maintaining_case / anchor.anchor_maintaining_case for github-slugger's maintainCase behavior.
let config = config.default()
|> config.with_custom_replacements([
#("&", " and "),
#("@", " at "),
#("%", " percent ")
])
glugify.slugify_with("Cats & Dogs @ 100%", config)
// -> Ok("cats-and-dogs-at-100-percent")// With transliteration (default)
glugify.slugify("Café naïve résumé")
// -> "cafe-naive-resume"
// Preserving Unicode
let unicode_config = config.default()
|> config.with_transliterate(False)
|> config.with_allow_unicode(True)
glugify.slugify_with("Café naïve résumé", unicode_config)
// -> Ok("café-naïve-résumé")Transliteration covers Latin-extended characters (e.g. ø, Ł, æ, ß, İ), Cyrillic, Greek, Arabic/Persian and Hebrew (basic consonantal romanization), typographic punctuation (smart quotes, dashes, ellipses), and common currency signs and symbols. Decomposed (NFD) input is normalized by mapping base characters and dropping combining marks. Characters with no known mapping — such as emoji or CJK — are stripped rather than causing an error:
glugify.slugify("10 Tips 🚀 for Gleam")
// -> "10-tips-for-gleam"
glugify.slugify("Привет мир")
// -> "privet-mir"
glugify.slugify("Don’t — “Stop”")
// -> "dont-stop"import glugify/locale
let config = config.default()
|> config.with_locale(locale.German)
glugify.slugify_with("Über München", config)
// -> Ok("ueber-muenchen") (default locale gives "uber-munchen")
let config = config.default()
|> config.with_locale(locale.Danish)
glugify.slugify_with("København på Ærø", config)
// -> Ok("koebenhavn-paa-aeroe")let config = config.default()
|> config.with_decamelize(True)
glugify.slugify_with("myAwesomeXMLParser", config)
// -> Ok("my-awesome-xml-parser")let config = config.default()
|> config.with_decode_html_entities(True)
glugify.slugify_with("Tom & Jerry – Classics", config)
// -> Ok("tom-and-jerry-classics")let config = config.default()
|> config.with_ignore(["#"])
glugify.slugify_with("C# and F# compared", config)
// -> Ok("c#-and-f#-compared")let config = config.default()
|> config.with_stop_words(["the", "a", "an", "and", "or"])
glugify.slugify_with("The Quick Brown Fox and the Lazy Dog", config)
// -> Ok("quick-brown-fox-lazy-dog")The library provides explicit error types for robust error handling:
import glugify/errors
case glugify.try_slugify("") {
Ok(slug) -> slug
Error(errors.EmptyInput) -> "Please provide some text"
Error(errors.TransliterationFailed(char)) -> "Cannot transliterate: " <> char
Error(errors.ConfigurationError(msg)) -> "Config error: " <> msg
}EmptyInputis returned when the input is empty or whitespace-only.TransliterationFailedis returned only when transliteration is disabled (with_transliterate(False)) and the input contains non-ASCII characters whileallow_unicodeisFalse. With transliteration enabled (the default), unmappable characters are stripped instead.ConfigurationErroris returned for invalid configuration, such as a separator longer than 10 characters.
| Test Case | Function | IPS (ops/sec) | Min Time (ms) | P99 Time (ms) |
|---|---|---|---|---|
| Simple text ("Hello World") | slugify | 44,937 | 0.019 | 0.033 |
| Simple text ("Hello World") | slugify_with_custom_config | 46,145 | 0.019 | 0.033 |
| Unicode text with emojis | slugify | 22,292 | 0.039 | 0.081 |
| Unicode text with emojis | slugify_with_custom_config | 22,328 | 0.041 | 0.073 |
| Long text (200+ chars) | slugify | 4,304 | 0.214 | 0.430 |
| Long text (200+ chars) | slugify_with_custom_config | 4,449 | 0.216 | 0.275 |
| Complex text (mixed case, symbols) | slugify | 7,399 | 0.129 | 0.162 |
| Complex text (mixed case, symbols) | slugify_with_custom_config | 7,278 | 0.131 | 0.166 |
Erlang Summary: Average of ~19,900 operations per second across all test cases.
| Test Case | Function | IPS (ops/sec) | Min Time (ms) | P99 Time (ms) |
|---|---|---|---|---|
| Simple text ("Hello World") | slugify | 5,391 | 0.112 | 1.614 |
| Simple text ("Hello World") | slugify_with_custom_config | 6,085 | 0.107 | 1.669 |
| Unicode text with emojis | slugify | 2,559 | 0.265 | 1.888 |
| Unicode text with emojis | slugify_with_custom_config | 2,224 | 0.290 | 2.099 |
| Long text (200+ chars) | slugify | 383 | 1.660 | 5.404 |
| Long text (200+ chars) | slugify_with_custom_config | 394 | 1.717 | 5.362 |
| Complex text (mixed case, symbols) | slugify | 663 | 0.955 | 4.523 |
| Complex text (mixed case, symbols) | slugify_with_custom_config | 661 | 0.961 | 4.833 |
JavaScript Summary: Average of ~2,300 operations per second across all test cases.
- The Erlang target significantly outperforms JavaScript (roughly 8x for most operations)
- Custom configurations add negligible overhead
- Performance decreases predictably with input length; long text (200+ characters) is the main bottleneck
- Character lookups use pattern matching rather than dictionary construction, so transliteration adds minimal cost
The benchmarks were run using gleamy_bench with 100ms duration and 10ms warmup per test (gleam run -m benchmark_runner). Results may vary depending on your specific use case and runtime environment.
Add glugify to your Gleam project:
gleam add glugifygleam test # Run the tests
gleam test --target javascript # Run the tests on the JavaScript target
gleam format # Format the code
gleam run -m benchmark_runner # Run the benchmarks
./docs/build-playground.sh # Rebuild the live playground bundle after changing src/The live playground is served by GitHub Pages from the docs/ folder.
Contributions are welcome! Please feel free to submit a Pull Request!
Further documentation can be found at https://hexdocs.pm/glugify.