Skip to content

Slugify — C# source

Generate clean, URL-safe slugs from any text with locale-aware Unicode transliteration. Accents, emoji, and punctuation are handled automatically - runs entirely in your browser.

This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// slugify — C# port: URL-safe slugs with locale-aware Unicode transliteration.
using System;
using System.Collections.Generic;
using System.Text;

/// <summary>Pure slug logic, ported from the canonical TS lib. Deterministic and
/// total: any input yields a slug, never an exception.</summary>
public static class Slugify
{
    /// <summary>Letter casing for the produced slug.</summary>
    public enum SlugCase { Lower, Preserve, Upper }

    /// <summary>Options mirror the TS SlugifyOptions; every field defaults.</summary>
    public sealed class Options
    {
        public string Separator = "-";
        public int MaxLength;                    // <= 0 = unlimited
        public SlugCase Case = SlugCase.Lower;
        public bool StripStopwords;
    }

    // Letters/ligatures NFKD does not decompose into an ASCII base + combining
    // mark. Accented Latin (á é ñ …) needs no entry: FormKD splits it and the
    // U+0300..U+036F strip below drops the diacritic.
    private static readonly Dictionary<char, string> Translit = new()
    {
        ['ß'] = "ss",
        ['æ'] = "ae", ['Æ'] = "ae", ['œ'] = "oe", ['Œ'] = "oe",
        ['ff'] = "ff", ['fi'] = "fi", ['fl'] = "fl", ['ffi'] = "ffi", ['ffl'] = "ffl", ['ſt'] = "st", ['st'] = "st",
        ['ð'] = "d", ['Ð'] = "d", ['þ'] = "th", ['Þ'] = "th", ['ø'] = "o", ['Ø'] = "o",
        ['ł'] = "l", ['Ł'] = "l", ['đ'] = "d", ['Đ'] = "d", ['ħ'] = "h", ['Ħ'] = "h",
    };

    private static readonly HashSet<string> Stopwords = new()
    {
        "the", "a", "an", "and", "or", "but", "of", "to", "in", "on", "at",
        "for", "with", "by", "from",
    };

    private static bool IsAsciiAlnum(char c) =>
        (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');

    /// <summary>Break text into clean ASCII words (transliterated, diacritics
    /// stripped, cased). Mirrors the TS tokenize().</summary>
    private static List<string> Tokenize(string text, Options o)
    {
        var t = new StringBuilder(text.Length);
        foreach (var ch in text) t.Append(Translit.TryGetValue(ch, out var rep) ? rep : ch.ToString());

        var ascii = new StringBuilder();
        foreach (var ch in t.ToString().Normalize(NormalizationForm.FormKD))
        {
            if (ch >= '̀' && ch <= 'ͯ') continue;        // combining diacritics
            ascii.Append(IsAsciiAlnum(ch) ? ch : ' ');             // collapse runs to spaces
        }

        var words = new List<string>();
        var cur = new StringBuilder();
        foreach (var ch in ascii.ToString().Trim().ToCharArray())
        {
            if (ch == ' ') { if (cur.Length > 0) { words.Add(cur.ToString()); cur.Clear(); } }
            else cur.Append(ch);
        }
        if (cur.Length > 0) words.Add(cur.ToString());

        for (var i = 0; i < words.Count; i++)
            words[i] = o.Case switch
            {
                SlugCase.Upper => words[i].ToUpperInvariant(),
                SlugCase.Lower => words[i].ToLowerInvariant(),
                _ => words[i],
            };

        if (o.StripStopwords)
            words.RemoveAll(w => Stopwords.Contains(w.ToLowerInvariant()));
        return words;
    }

    // Truncate to max chars at the last whole-word boundary (hard cut when the
    // separator is empty or absent from the head).
    private static string TruncateAtWord(string slug, string separator, int max)
    {
        if (slug.Length <= max) return slug;
        var cut = slug.Substring(0, max);
        if (separator.Length == 0) return cut;
        var last = cut.LastIndexOf(separator, StringComparison.Ordinal);
        return last > 0 ? cut.Substring(0, last) : cut;
    }

    /// <summary>Convert arbitrary text into a URL-safe slug.</summary>
    public static string SlugifyText(string text, Options? o = null)
    {
        o ??= new Options();
        var slug = string.Join(o.Separator, Tokenize(text, o));
        return o.MaxLength > 0 ? TruncateAtWord(slug, o.Separator, o.MaxLength) : slug;
    }

    /// <summary>Slugify each line independently (batch mode).</summary>
    public static List<string> SlugifyLines(string text, Options? o = null)
    {
        o ??= new Options();
        var out_ = new List<string>();
        foreach (var line in text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
            out_.Add(SlugifyText(line, o));
        return out_;
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →