Agent-First Slug
Rust slug generation with explicit caller configuration for path and URL path segments.
Ask your agent: "Add agent-first-slug to my project and use it to slugify titles for URL path segments."
Start by choosing the target surface: a local filesystem path segment, a URL path segment, or a legacy slug format you need to preserve. The examples below show complete config values so the behavior is visible at the call site.
Install the Library
Install the CLI
# prebuilt binary
&&
# or from crates.io
Prebuilt archives are also available from GitHub Releases.
CLI
afslug generates and validates slugs, emitting one AFDATA protocol event per
run. JSON is the default; YAML and plain output are also available.
# {"kind":"result","result":{"changed_from_input":true,"code":"slugify","slug":"hello-世界"},"trace":{}}
# kind=result result.changed_from_input=true result.code=slugify result.slug=hello-world
slugify exposes the SlugConfig surface as flags —
delimiter, case, truncation, character set, dot handling, validation, and an
empty-slug fallback (afslug slugify --help lists them); validate checks an
existing value as a local or URL path segment. Transliteration stays
library-only: its static replacement map cannot be built from CLI arguments.
Agent Skill
Use skills/agent-first-slug/SKILL.md to
teach a coding agent when to choose the Rust library or the default-only
afslug CLI, and how to preserve stable identifier behavior.
Default Unicode Slugs
use ;
let config = default;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
Unicode letters and numbers are preserved by default:
use ;
let config = default;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
Local Filesystem Path Segment
Use this when the slug will be one segment in a local path. Slugify each segment separately; do not pass a full path through one slug call.
Requirements for this target:
- Reject empty output unless the caller provides a fallback.
- Reject
/,\\, Unicode whitespace, and control characters. - Reject
.and..to avoid current-directory and parent-directory meanings. - Prefer replacing dots unless the caller explicitly wants dots in filenames.
- Choose ASCII-only if the path must be portable across legacy filesystems or tools.
use ;
let config = SlugConfig ;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
ASCII-only path segment with fallback:
use ;
let config = SlugConfig ;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
URL Path Segment
Use this when the slug will be one segment in a URL path. The returned slug is raw UTF-8; percent-encode it or use a URL library's path-segment API when building the final URL.
Requirements for this target:
- Reject empty output unless the caller provides a fallback.
- Reject
/,?,#, raw%, Unicode whitespace, controls, and\\. - Reject
.and..for route safety. - Preserve dots between decimal digits if version numbers matter.
- Do not manually concatenate unescaped slugs into URLs.
use ;
let config = SlugConfig ;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
Dot Handling
use ;
let replace_all_dots = SlugConfig ;
let preserve_all_dots = SlugConfig ;
let preserve_version_dots = SlugConfig ;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
Transliteration
Transliteration is caller-provided, so legacy behavior can be expressed without a named preset in the library.
use ;
static MAP: & = &;
let config = SlugConfig ;
assert_eq!;
# Ok::
Truncation And Empty Output
max_slug_chars counts Unicode scalar values; any trailing delimiter the cut
exposes is then stripped. Empty handling runs after truncation.
A fallback is inserted as written rather than run through the pipeline, but
UseFallbackSlug still requires it to satisfy the same configuration —
character set, delimiter, dot policy, case and length. A fallback that does not
is a configuration error, not a slug: without that check an ASCII-only,
length-capped configuration could return an arbitrary Unicode string and report
it as validated. UseVerbatimFallbackSlug waives it for a value that has to
match something already stored, and checks only the target surface.
use ;
let truncated = SlugConfig ;
let fallback = SlugConfig ;
let legacy = SlugConfig ;
assert_eq!;
assert_eq!;
assert_eq!;
// Too long for this configuration's own budget, so it is refused rather than
// returned as if it had been generated.
let over_budget = SlugConfig ;
assert!;
# Ok::
What Changed, And What To Check Before Upgrading
Two rules that affect generated slugs changed, because both let a configuration mean something other than what it said. If you have slugs already stored, run the old and new versions over your corpus and diff before upgrading.
Case mapping now runs before filtering. It ran after, and Unicode case
mapping is not one scalar for one scalar: İ lowercases to i plus a combining
dot, and that dot ended up in the slug even though no character set here would
have kept it. İstanbul was i̇stanbul; it is now i-stanbul. Only inputs
whose case mapping expands are affected — every scalar in a slug is now one the
character set admits, so allowed_character_set describes the output again.
A replacement_delimiter the configuration would keep is refused. With a
as the delimiter, alpha beta and lpha beta both produced lphabet: the run
boundary was skipped because the output already ended in a, and the trim then
ate real letters off real words. Configurations using -, _, ~ or any other
character the filter removes are unaffected.
Nothing normalizes input, then or now — see the skill for what that means for a stable-identifier contract.
Validation Only
use ;
assert_eq!;
assert_eq!;
assert_eq!;
License
MIT