Match a URL slug
Validate kebab-case slugs the way routers expect them: lowercase words separated by single hyphens, never leading, trailing or doubled.
Last updated
Matches
- my-first-post
- v2
- a-1-b-2
Doesn’t match
- -leading
- trailing-
- double--dash
- Has-Caps
Why it’s written this way
One alphanumeric run, then zero or more "-run" groups. Because the hyphen only exists INSIDE the repeating group, a leading hyphen, trailing hyphen or double hyphen has nowhere to match — three validation rules fall out of the structure for free.
The ^…$ anchors make it a whole-field validator; drop them to find slug-shaped runs inside longer text instead.
Edge cases to know
- →Uppercase fails by design — slugify input before validating rather than loosening the pattern.
- →Underscores fail too; if your routes use them, swap the hyphen for [-_].
- →It puts no ceiling on length — enforce that separately if your CMS does.
Part by part
- ^
- Anchors at the start of the string. Without it the pattern could match a valid slug sitting inside an invalid one.
- [a-z0-9]+
- One or more lowercase letters or digits — the first segment. Uppercase is excluded deliberately, not accidentally.
- (?:
- Opens a non-capturing group, so repeating the segment does not fill your match array with throwaway captures.
- -[a-z0-9]+
- A single hyphen followed by another segment. The hyphen can only ever appear between segments, never at an edge.
- )*
- Zero or more additional segments, which is what makes a one-word slug like "about" valid.
- $
- Anchors at the end. Together with ^ this makes the pattern a validator rather than a finder.
Common mistakes
^[a-z0-9-]+$Putting the hyphen inside the character class is the most common shortcut, and it accepts exactly what slug rules exist to forbid: leading hyphens, trailing hyphens and doubled hyphens all pass.
^[a-z0-9]+(?:-[a-z0-9]+)*$[a-z0-9]+(?:-[a-z0-9]+)*Dropping the anchors turns a validator into a finder. "Hello_World" now passes, because the pattern happily matches the "ello" inside it and reports success.
^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$Allowing uppercase means "My-Post" validates, but on a case-sensitive server it resolves to a different URL than "my-post" — so you end up with two pages holding the same content.
Questions
Does this allow underscores in a slug?
No. The only characters permitted are lowercase letters, digits and the hyphen used as a separator. If your CMS emits underscores, convert them to hyphens before validating — search engines treat a hyphen as a word break and an underscore as a joiner.
Why does it reject a trailing hyphen?
Because the hyphen only appears inside the repeated group, where it must be followed by at least one more character. A trailing hyphen usually means the title ended in punctuation that was stripped, and it produces ugly, duplicate-prone URLs.
Should I lowercase the string before testing it?
Yes. This pattern validates rather than normalises, so it will simply reject anything with capitals. Lowercase and transliterate first, then validate — that order gives you a clean slug instead of a rejection you have to explain.
Does it limit how long a slug can be?
No, there is no upper bound in the pattern. Length is a product decision rather than a correctness one, so enforce it separately — most teams cap slugs somewhere between 60 and 100 characters to keep URLs readable.