Slug
URL-safe slug generation with two normalization paths and a uniqueness helper.
The default ASCII path uses Unicode NFKD decomposition + combining-mark strip (café → cafe) and drops anything outside [a-zA-Z0-9]. The preserveUnicode: true path uses NFC and only drops Unicode punctuation, symbols, and separators — Cyrillic, Greek, CJK, and other scripts pass through. Operators with non-Latin user content opt into preserveUnicode.
b.slug is a callable function with the rest of the API hung off it (callable-namespace pattern): b.slug.create builds a bound slugger pre-configured with operator opts, and b.slug.unique resolves the first un-taken candidate against an operator-supplied isUsed predicate (numeric suffixes -2, -3, … on collision).
Validation policy: opts and title are validated at the call site (throw on bad input). Titles that normalize to empty are tolerant — opts.fallback is returned instead of throwing. unique() exhausting maxAttempts throws SlugError.
Reserved web slugs (admin, api, login, …) live on b.slug.RESERVED as a mutable Set; operators extend it once at boot and pass it into their isUsed predicate.
b.slug(title, callOpts) #
{
separator: string, // single-char join between tokens (default "-")
lowercase: boolean, // lowercase output (default true, locale-independent)
maxLength: number, // hard cap on output length, or null for none (default 80)
preserveUnicode: boolean, // keep non-ASCII letters/digits (default false)
fallback: string, // returned when title normalizes to empty (default "")
}
Slugify a title. The default path produces lowercase ASCII separated by -: accents fold (café → cafe), runs of non-alphanumerics collapse to a single separator, and the result is trimmed of leading/trailing separators and capped at maxLength (truncating at a separator boundary when possible). Empty results return opts.fallback.
preserveUnicode: true keeps letters and digits in any script and only drops punctuation/symbols/separators — the right choice for non-Latin user content.
b.slug("Hello, World!");
// → "hello-world"
b.slug("café résumé");
// → "cafe-resume"
b.slug("Привет мир", { preserveUnicode: true });
// → "привет-мир"
b.slug("a".repeat(200), { maxLength: 10 });
// → "aaaaaaaaaa"
b.slug("---", { fallback: "untitled" });
// → "untitled"
b.slug.create(creatorOpts) #
{
separator: string, // single-char join between tokens (default "-")
lowercase: boolean, // lowercase output (default true)
maxLength: number, // hard cap on output length, or null for none (default 80)
preserveUnicode: boolean, // keep non-ASCII letters/digits (default false)
fallback: string, // returned when title normalizes to empty (default "")
}
Build a bound slugger pre-configured with operator opts. Returns a function with the same signature as b.slug — per-call opts merge over the bound opts, so the operator picks defaults once at boot and call sites stay short. Useful when one section of the app slugs with non-default settings (longer maxLength, Unicode-preserving, custom separator).
var titleSlug = b.slug.create({ maxLength: 60, preserveUnicode: true });
titleSlug("Привет мир");
// → "привет-мир"
titleSlug("Hello, World!");
// → "hello-world"
// Per-call opts override creator opts:
titleSlug("Hello, World!", { separator: "_" });
// → "hello_world"
b.slug.unique(title, isUsed, callOpts) #
{
separator: string, // single-char join between tokens (default "-")
lowercase: boolean, // lowercase output (default true)
maxLength: number, // hard cap on output length, or null for none (default 80)
preserveUnicode: boolean, // keep non-ASCII letters/digits (default false)
fallback: string, // returned when title normalizes to empty (default "")
maxAttempts: number, // total tries including bare base (default 100)
start: number, // first numeric suffix (default 2)
suffixSeparator: string, // separator between base and suffix (default opts.separator)
}
Resolve the first un-taken slug for title against an operator-supplied isUsed(candidate) predicate (sync or async). The bare slug is tried first; on collision the function appends a numeric suffix (-2, -3, …) and re-checks until isUsed returns falsy or maxAttempts is exhausted. When the suffix would push past maxLength, the base is truncated at a separator boundary so the final candidate fits. Throws SlugError on exhaustion.
var taken = new Set(["hello-world", "hello-world-2"]);
async function isUsed(cand) { return taken.has(cand); }
var s1 = await b.slug.unique("Hello, World!", isUsed);
// → "hello-world-3"
var s2 = await b.slug.unique("Brand New Title", isUsed);
// → "brand-new-title"
// Custom suffix separator + start index:
var s3 = await b.slug.unique("Hello, World!", isUsed, {
suffixSeparator: "_",
start: 10,
});
// → "hello-world_10"
Last updated 2026-08-08T16:39:49.652Z by seeder.