Article guideContents, topics, tags, and RSS
A slug turns a title such as ICH MUẞ EINIGE CRÈME BRÛLÉE HABEN into a readable URL segment such as ich-muss-einige-creme-brulee-haben.
The important part is not only replacing spaces. A useful slug function needs an explicit policy for Unicode, repeated separators, empty input, and maximum length.
Decide what your slugs support
This example creates lowercase ASCII slugs. It uses .NET Unicode normalization to separate many Latin letters from their diacritics, removes the combining marks, transliterates a small set of letters, and converts every other boundary to a hyphen.
ASCII is a product choice, not an SEO requirement. Modern URLs can contain Unicode. If your content spans languages that cannot be represented sensibly with Latin characters, keep Unicode slugs or use a tested transliteration library instead of silently dropping letters.
The slug function
using System.Globalization;
using System.Text;
public static class Slug
{
public static string Create(string? value, int maxLength = 80)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
if (maxLength < 1)
{
throw new ArgumentOutOfRangeException(nameof(maxLength));
}
var normalized = value
.Trim()
.ToLowerInvariant()
.Normalize(NormalizationForm.FormD);
var result = new StringBuilder(normalized.Length);
foreach (var character in normalized)
{
var category = CharUnicodeInfo.GetUnicodeCategory(character);
if (category == UnicodeCategory.NonSpacingMark)
{
continue;
}
var replacement = character switch
{
'ß' => "ss",
'æ' => "ae",
'ø' => "o",
'ð' => "d",
'þ' => "th",
'ł' => "l",
_ when character <= 127 && char.IsLetterOrDigit(character) =>
character.ToString(),
_ => null,
};
if (replacement is not null)
{
foreach (var replacementCharacter in replacement)
{
if (result.Length == maxLength)
{
break;
}
result.Append(replacementCharacter);
}
}
else if (result.Length > 0 && result[^1] != '-' && result.Length < maxLength)
{
result.Append('-');
}
if (result.Length == maxLength)
{
break;
}
}
return result.ToString().Trim('-');
}
}
The normalization step uses Form D, which decomposes characters such as è into a base letter and a combining mark. The code discards the combining mark while retaining the base letter. The explicit switch handles letters that do not decompose into the ASCII result we want.
Check the behavior
var slug = Slug.Create("ICH MUẞ EINIGE CRÈME BRÛLÉE HABEN");
Console.WriteLine(slug);
// ich-muss-einige-creme-brulee-haben
Add tests for the rules your application depends on:
[Theory]
[InlineData("Hello, world!", "hello-world")]
[InlineData(" repeated spaces ", "repeated-spaces")]
[InlineData("Crème brûlée", "creme-brulee")]
[InlineData("Straße", "strasse")]
[InlineData("", "")]
public void Creates_expected_slug(string input, string expected)
{
Assert.Equal(expected, Slug.Create(input));
}
If you use generated slugs as permanent public URLs, store the chosen slug with the record. Changing the title or transliteration rules later should not silently change an already published URL. ASP.NET Core can also apply slug rules during link generation with an outbound parameter transformer, but the same stability rule still applies.
