PatternRegex

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

Pattern
^[a-z0-9]+(?:-[a-z0-9]+)*$
Open in Regex Tester & Builder

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.

Related in Patterns