Quick answer
The standard slug regex is ^[a-z0-9]+(?:-[a-z0-9]+)*$. It accepts lowercase letters and digits in groups separated by single hyphens, so hello-world and post-123 match, while Hello-World, hello--world, -hello and hello_world do not. Generate slugs with a slugify function, then validate them with this pattern before saving so URLs stay consistent.
- Tested examples
- Token-by-token breakdown
- Code in 5 languages
- Live tester
How to use the URL slug regex
- 1Copy the pattern with Copy pattern in the tester, or take a ready-made snippet for your language from the code section below.
- 2Check the sample lines in Test string: every line the URL slug regex accepts is highlighted, rejected lines stay plain.
- 3Paste your own values one per line to see which pass, and read Explanation for what each part of the pattern does.
- 4Open the Code tab for JavaScript, Python, Go, C#, PHP or Dart with the pattern translated and escaped.
- 5Validate again on the server: client-side checks give users quick feedback, not security.
How does the URL slug regex work?
Read it left to right. Each piece below matches one part of the input:
^Start of the string.[a-z0-9]+The first word: one or more lowercase letters or digits.(?:-[a-z0-9]+)*Any number of further words, each introduced by exactly one hyphen.$End of the string, so a trailing hyphen fails.
Which strings does the URL slug regex match?
The whole input must match, from the first character to the last. Every row below is checked against the pattern by our automated test suite before each release.
| Input | Matches? | Why |
|---|---|---|
hello-world | Yes | Two words, one hyphen. |
regex-tester | Yes | A typical tool path. |
post-123 | Yes | Digits are words too. |
2026-roadmap | Yes | A slug may start with a digit. |
a | Yes | A single character. |
Hello-World | No | Uppercase letters. Lowercase slugs avoid duplicate URLs. |
hello--world | No | Doubled hyphen. |
-hello | No | Leading hyphen. |
hello- | No | Trailing hyphen. |
hello_world | No | Underscores are not word separators for search engines. |
hello world | No | Spaces would be encoded as %20. |
How do I turn a title into a slug?
Normalize, lowercase, replace anything that is not a letter or digit with a hyphen, then trim:
function slugify(title) {
return title
.normalize("NFKD") // é → e + combining accent
.replace(/[\u0300-\u036f]/g, "") // drop the accents
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-") // runs of anything else → one hyphen
.replace(/^-+|-+$/g, ""); // no hyphen at either end
}
slugify("Crème Brûlée: 10 Tips!"); // "creme-brulee-10-tips"The output always matches the slug regex (or is empty, which you should reject). Run the regex on slugs that users edit by hand as well, since the admin form is where doubled hyphens and capitals sneak in.
Why hyphens and lowercase?
Google's URL structure guidelines recommend hyphens to separate words, because underscores join words together for search. Lowercase avoids a quieter problem: most servers treat paths as case-sensitive, so /Blog/Post and /blog/post would be two URLs with the same content.
For non-Latin titles, either transliterate (a library such as slugify or transliteration handles Cyrillic, Greek and more) or allow Unicode letters with ^[\p{Ll}\p{N}]+(?:-[\p{Ll}\p{N}]+)*$ and the u flag; browsers display those paths readably even though they are percent-encoded on the wire.
How do I use the URL slug regex in JavaScript, Python, Go, C# and PHP?
The pattern is translated for each engine rather than pasted as is: named groups, anchors, escapes, shorthand classes and flags are adjusted so each snippet behaves as closely to the JavaScript version as its engine allows. Differences that remain (Go's RE2 has no lookarounds or backreferences, for example) are called out under each one. The Code tab in the tester above also exports Dart, and updates as you edit the pattern.
JavaScript and TypeScript
const regex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
function isValid(value) {
return regex.test(value);
}
console.log(isValid("hello-world"));Python
import re
pattern = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*\Z")
def is_valid(value: str) -> bool:
return pattern.search(value) is not None
print(is_valid("hello-world"))- JavaScript's $ only matches at the very end of the string. Python's $ also matches before a trailing newline, so the pattern uses \Z instead.
Go
package main
import (
"fmt"
"regexp"
)
func main() {
re := regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
fmt.Println(re.MatchString("hello-world"))
}C#
using System;
using System.Text.RegularExpressions;
// The timeout stops a pathological input from hanging the thread (ReDoS).
var regex = new Regex(@"^[a-z0-9]+(?:-[a-z0-9]+)*\z", RegexOptions.None, TimeSpan.FromSeconds(1));
Console.WriteLine(regex.IsMatch("hello-world"));- JavaScript's $ only matches at the very end of the string. .NET's $ also matches before a trailing newline, so the pattern uses \z instead.
PHP
<?php
$pattern = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/D';
function isValid(string $value, string $pattern): bool
{
return preg_match($pattern, $value) === 1;
}
var_dump(isValid('hello-world', $pattern));- The D modifier makes $ match only at the very end, like JavaScript (PCRE's $ also matches before a trailing newline).
Which related regex patterns are useful?
- Regex for URL Validation: http and https links with a real domain, optional port, path, query and fragment.
- Regex for Usernames: 3–16 characters, starts with a letter, single separators between letters and digits.
- Regex for Alphanumeric Characters: Letters A–Z and digits only, with a Unicode variant for every script.
- Regex for Domain Names: Hostnames with valid labels, a 253-character limit and Punycode support.
Browse all 25 patterns in the regex pattern library, or build your own in the regex tester.
Frequently asked questions
Last updated by the Playxoft engineering team.