const MAX_SLUG_LENGTH = 96; /** * Turns arbitrary text into a URL-safe slug: lowercase ASCII words joined * by single hyphens. Returns "" when nothing usable remains, so callers * can decide on a fallback. */ export function slugify(input: string): string { return input .normalize("NFKD") .replace(/[̀-ͯ]/g, "") // strip combining diacritics .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, MAX_SLUG_LENGTH) .replace(/-+$/g, ""); } /** * Finds a free slug by appending -2, -3, ... to the base while `isTaken` * reports a conflict. The base falls back to "untitled" when empty. */ export async function ensureUniqueSlug( base: string, isTaken: (slug: string) => Promise, ): Promise { const root = base || "untitled"; let candidate = root; for (let suffix = 2; await isTaken(candidate); suffix++) { if (suffix > 500) { throw new Error(`Could not find a free slug for "${root}"`); } candidate = `${root}-${suffix}`; } return candidate; }