Expand description
§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
cargo add agent-first-slug --no-default-features§Install the CLI
# prebuilt binary
brew install agentfirstkit/tap/afslug # macOS / Linux
scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket && scoop install afslug # Windows
# or from crates.io
cargo install agent-first-slugPrebuilt 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.
afslug slugify "Hello, 世界!"
# {"kind":"result","result":{"changed_from_input":true,"code":"slugify","slug":"hello-世界"},"trace":{}}
afslug slugify "Hello, World!" --output plain
# kind=result result.changed_from_input=true result.code=slugify result.slug=hello-world
afslug validate "my-slug" --policy url-pathslugify 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 agent_first_slug::{slugify, SlugConfig};
let config = SlugConfig::default();
assert_eq!(slugify("Hello", &config)?.slug, "hello");
assert_eq!(slugify("Hello World", &config)?.slug, "hello-world");
assert_eq!(slugify("Hello, world!!!", &config)?.slug, "hello-world");
assert_eq!(slugify("-- already -- spaced --", &config)?.slug, "already-spaced");
assert_eq!(slugify("!!!", &config)?.slug, "");Unicode letters and numbers are preserved by default:
use agent_first_slug::{slugify, SlugConfig};
let config = SlugConfig::default();
assert_eq!(
slugify("現在的Nobody,未來的Somebody!", &config)?.slug,
"現在的nobody-未來的somebody"
);
assert_eq!(
slugify("牙好,胃口就好,身体倍儿棒,吃嘛嘛香。", &config)?.slug,
"牙好-胃口就好-身体倍儿棒-吃嘛嘛香"
);
assert_eq!(slugify("お元気ですか?", &config)?.slug, "お元気ですか");§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 agent_first_slug::{
slugify, AllowedCharacterSet, DotHandlingPolicy, EmptyOutputPolicy, SlugConfig,
SlugValidationPolicy, TransliterationPolicy,
};
let config = SlugConfig {
replacement_delimiter: '-',
lowercase_enabled: true,
max_slug_chars: None,
allowed_character_set: AllowedCharacterSet::UnicodeAlphanumericCharacters,
dot_handling_policy: DotHandlingPolicy::ReplaceAllDots,
transliteration_policy: TransliterationPolicy::None,
validation_policy: SlugValidationPolicy::LocalPathSegment,
empty_output_policy: EmptyOutputPolicy::UseFallbackSlug("fallback".to_string()),
};
assert_eq!(slugify("Ubuntu 16.04", &config)?.slug, "ubuntu-16-04");
assert_eq!(slugify("你好,世界", &config)?.slug, "你好-世界");
assert_eq!(slugify("!!!", &config)?.slug, "fallback");ASCII-only path segment with fallback:
use agent_first_slug::{
slugify, AllowedCharacterSet, DotHandlingPolicy, EmptyOutputPolicy, SlugConfig,
SlugValidationPolicy, TransliterationPolicy,
};
let config = SlugConfig {
replacement_delimiter: '-',
lowercase_enabled: true,
max_slug_chars: None,
allowed_character_set: AllowedCharacterSet::AsciiAlphanumericCharacters,
dot_handling_policy: DotHandlingPolicy::ReplaceAllDots,
transliteration_policy: TransliterationPolicy::None,
validation_policy: SlugValidationPolicy::LocalPathSegment,
empty_output_policy: EmptyOutputPolicy::UseFallbackSlug("fallback".to_string()),
};
assert_eq!(slugify("Hello 世界", &config)?.slug, "hello");
assert_eq!(slugify("Ubuntu 16.04", &config)?.slug, "ubuntu-16-04");
assert_eq!(slugify("你好,世界", &config)?.slug, "fallback");§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 agent_first_slug::{
slugify, AllowedCharacterSet, DotHandlingPolicy, EmptyOutputPolicy, SlugConfig,
SlugValidationPolicy, TransliterationPolicy,
};
let config = SlugConfig {
replacement_delimiter: '-',
lowercase_enabled: true,
max_slug_chars: None,
allowed_character_set: AllowedCharacterSet::UnicodeLettersAndDecimalDigits,
dot_handling_policy: DotHandlingPolicy::PreserveDotsBetweenDecimalDigits,
transliteration_policy: TransliterationPolicy::None,
validation_policy: SlugValidationPolicy::UrlPathSegment,
empty_output_policy: EmptyOutputPolicy::KeepEmptySlug,
};
assert_eq!(slugify("Ubuntu 16.04", &config)?.slug, "ubuntu-16.04");
assert_eq!(slugify("T.U.S.F.G.E.3.0.8", &config)?.slug, "t-u-s-f-g-e-3.0.8");
assert_eq!(slugify(".18 increased ! ", &config)?.slug, "18-increased");
assert_eq!(slugify("お元気ですか?", &config)?.slug, "お元気ですか");§Dot Handling
use agent_first_slug::{slugify, DotHandlingPolicy, SlugConfig};
let replace_all_dots = SlugConfig {
dot_handling_policy: DotHandlingPolicy::ReplaceAllDots,
..SlugConfig::default()
};
let preserve_all_dots = SlugConfig {
dot_handling_policy: DotHandlingPolicy::PreserveAllDots,
..SlugConfig::default()
};
let preserve_version_dots = SlugConfig {
dot_handling_policy: DotHandlingPolicy::PreserveDotsBetweenDecimalDigits,
..SlugConfig::default()
};
assert_eq!(slugify("Ubuntu 16.04", &replace_all_dots)?.slug, "ubuntu-16-04");
assert_eq!(slugify("A.B..C", &preserve_all_dots)?.slug, "a.b..c");
assert_eq!(slugify("T.U.S.F.G.E.3.0.8", &preserve_version_dots)?.slug, "t-u-s-f-g-e-3.0.8");§Transliteration
Transliteration is caller-provided, so legacy behavior can be expressed without a named preset in the library.
use agent_first_slug::{
slugify, AllowedCharacterSet, SlugConfig, TransliterationPolicy,
};
static MAP: &[(&str, &str)] = &[("Æ", "AE"), ("東京", "Tokyo")];
let config = SlugConfig {
allowed_character_set: AllowedCharacterSet::AsciiAlphanumericCharacters,
transliteration_policy: TransliterationPolicy::StaticReplacementMap(MAP),
..SlugConfig::default()
};
assert_eq!(slugify("Æther 東京", &config)?.slug, "aether-tokyo");§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 agent_first_slug::{slugify, EmptyOutputPolicy, SlugConfig, SlugError};
let truncated = SlugConfig {
max_slug_chars: Some(8),
..SlugConfig::default()
};
let fallback = SlugConfig {
max_slug_chars: Some(8),
empty_output_policy: EmptyOutputPolicy::UseFallbackSlug("untitled".to_string()),
..SlugConfig::default()
};
let legacy = SlugConfig {
max_slug_chars: Some(8),
empty_output_policy: EmptyOutputPolicy::UseVerbatimFallbackSlug(
"Legacy Name".to_string(),
),
..SlugConfig::default()
};
assert_eq!(slugify("Long Example", &truncated)?.slug, "long-exa");
assert_eq!(slugify("!!!", &fallback)?.slug, "untitled");
assert_eq!(slugify("!!!", &legacy)?.slug, "Legacy Name");
// 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 {
empty_output_policy: EmptyOutputPolicy::UseFallbackSlug("far-too-long".to_string()),
..fallback
};
assert!(matches!(
slugify("!!!", &over_budget),
Err(SlugError::FallbackViolatesConfig { .. })
));§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 agent_first_slug::{validate_slug, SlugError, SlugValidationPolicy};
assert_eq!(validate_slug("safe-name", SlugValidationPolicy::LocalPathSegment), Ok(()));
assert_eq!(
validate_slug("a/b", SlugValidationPolicy::LocalPathSegment),
Err(SlugError::PathSegmentSeparator { character: '/' })
);
assert_eq!(
validate_slug("a?b", SlugValidationPolicy::UrlPathSegment),
Err(SlugError::UrlPathSegmentReservedCharacter { character: '?' })
);§License
MIT
Structs§
- Slug
Config - Rules used by
slugify. - Slug
Result - Slug generation result.
Enums§
- Allowed
Character Set - Character sets that can pass through the slug filter.
- DotHandling
Policy - Dot handling before all other filtered characters become delimiters.
- Empty
Output Policy - Behavior when the generated slug is empty.
- Slug
Error - Errors returned by slug generation or validation.
- Slug
Validation Policy - Optional validation applied after slug generation and empty-output handling.
- Transliteration
Policy - Transliteration applied before character filtering.
Functions§
- slugify
- Generate a slug from
inputusing explicit caller-provided rules. - validate_
slug - Validate
valueaccording to a standalone validation policy.