Slug Input

Keeps URL slugs clean and stable: proper transliteration, a full-URL readback, collision checks, and a warning before editing a live slug.

StopsLive URL slug breaks links

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/slug-input.json

The files land in your project. They're yours to edit.

Usage

slug-input-usage.tsx
import { SlugInput } from "@/components/ui/slug-input"

<SlugInput
  title={title}
  baseUrl="https://acme.com/blog/"
  publishedSlug="pricing-update"
  defaultValue="pricing-update-2026"
  checkAvailability={check}
/>

What it catches

  • Lossy slugify (ß, é and similar), by transliterating first
  • Slugs already taken, with the next free one suggested
  • Edits to a published slug that would break links
  • Removing the redirect from the old slug
  • Slugs too long, cut at a word boundary

Props

SlugInput props
PropTypeDefault
title

The title the slug follows until the user edits the slug (or it has been published).

string""
label

Visible label.

string"URL slug"
value

Controlled slug.

string—
defaultValue

Initial slug when uncontrolled. Defaults to the published slug, or one made from the title.

string—
onValueChange

Called with the slug on every change.

(value: string) => void—
baseUrl

Where the slug lives, e.g. "https://acme.com/blog/".

string"https://example.com/blog/"
publishedSlug

The slug that is live now. Changing away from it warns that links will break.

string—
keepRedirect

Controlled "keep a redirect from the old URL" choice.

boolean—
defaultKeepRedirect

Initial redirect choice when uncontrolled. On by default, like Shopify's pre-ticked box.

booleantrue
onKeepRedirectChange

Called when the redirect choice changes.

(keep: boolean) => void—
checkAvailability

Resolves `true` when the slug is free. Called after a pause in typing; stale answers are ignored.

(slug: string) => boolean | Promise<boolean>—
maxLength

Most characters in a slug.

number60
breaksLinksText

Warning after a published slug changes; `{path}` is the old path.

string"Changing this breaks links to {path}."
redirectLabel

Label of the redirect checkbox; `{path}` is the old path.

string"Redirect {path} to the new URL"
noRedirectText

Shown when the redirect is unticked; `{path}` is the old path.

string"Visitors to {path} will get a 404."
restoreLabel

Label of the one-tap fix that restores the published slug.

string"Keep the old slug"
takenText

Shown when `checkAvailability` says the slug is taken.

string"That URL is already used."
useSuggestionLabel

Label of the fix that applies the next free slug; `{slug}` is filled in.

string"Use {slug}"
availableText

Shown when the slug is free.

string"Available"
checkingText

Shown while the check runs.

string"Checking…"
checkErrorText

Shown when the check fails.

string"Couldn't check if this URL is free."
fromTitleLabel

Label of the button that goes back to following the title.

string"Match the title"

* required. Also accepts the native props of <input> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Visible <label> on the input; status via aria-describedby
  • Availability and warnings render in a polite live region
  • Keep-redirect is a labelled checkbox; icons are aria-hidden

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Enforce slug uniqueness with a database constraint and create the old→new redirect server-side.

  • Collision check depends on your checkAvailability
  • Transliteration covers Latin-script letters; other scripts are dropped
  • Creating the redirect is your server's job

What it stops

Live URL slug breaks links

Also known as

slug · permalink · url slug · blog post · cms · seo url · handle · redirect

Field type

Dependencies

Pairs well with