Blog/developer

What Is a URL Slug? Examples, Rules, and Best Practices

By Yurlie AdminSeptember 18, 20268 min read 3 views
What Is a URL Slug? Examples, Rules, and Best Practices

Overview and Problem Statement

A URL slug is the human-readable segment of a URL that identifies a specific page, typically placed after the domain and any category path. In https://example.com/blog/what-is-a-url-slug, the slug is what-is-a-url-slug. It replaces an opaque identifier (a database row ID, a UUID, or a content hash) with a short, descriptive string that a user, a search engine crawler, or a log-parsing script can read without dereferencing it first.

Slugs matter for three overlapping reasons: they give search engines a readable keyword signal in the URL itself, they let users predict a page's content before clicking, and they make server logs and analytics dashboards easier to scan than a column full of UUIDs. Getting the generation rules wrong, however, produces URLs that break routing, collide with existing content, or fail silently on non-ASCII input.

Core Concepts

Anatomy of a Valid Slug

RFC 3986 defines the URI syntax and designates a specific set of unreserved characters, ALPHA, DIGIT, and the four symbols - . _ ~, that never require percent-encoding inside a URI. A slug could technically use any of these, but convention narrows the set further for readability and consistency:

  • Lowercase ASCII letters (a to z) and digits (0 to 9) only
  • A single hyphen (-) as the word separator, never an underscore or a raw space
  • No leading, trailing, or consecutive hyphens
  • Every other character, including ., _, and ~, is either stripped or transliterated before the slug is generated, even though RFC 3986 would allow them unescaped

Restricting the character set this tightly is what keeps a slug identical whether it is typed by hand, pasted into a chat client, or displayed in a search result snippet.

Common Pitfalls and Edge Cases

Four issues account for most slug-related bugs in production systems:

  • Case sensitivity. RFC 3986 treats the path component of a URI as case-sensitive (only the scheme and host are case-insensitive), so /blog/My-Post and /blog/my-post are technically different resources. Without server-side normalization to lowercase, this produces duplicate-content issues for search engines and cache-key fragmentation at the CDN layer.
  • Non-ASCII input. A title like "Cafe Guide" with an accented e needs transliteration (stripping the diacritic to produce cafe-guide) before slugification, not direct percent-encoding of the raw UTF-8 bytes. Percent-encoding an accented character still produces a technically valid URL, but one that is unreadable in a browser's address bar and inconsistent across normalization forms (NFC vs NFD).
  • Reserved path collisions. A slug that happens to match an existing route segment, admin, api, or tools, can silently shadow real application routes depending on how the router resolves path precedence. Slug generation should check against a reserved-words list, not just against existing content slugs.
  • Uniqueness after publication. Once a slug is live, changing it breaks every external bookmark and backlink pointing at the old path unless the change ships alongside a 301 redirect from the old slug to the new one.

Slug-Only URLs vs Hybrid ID+Slug URLs

Two URL identifier strategies dominate in practice, and the choice determines how much redirect infrastructure a slug change requires.

A pure slug URL, /blog/what-is-a-url-slug, uses the slug as the sole lookup key. This keeps URLs short and fully descriptive, but the slug column now carries all the weight of the uniqueness and redirect concerns above: changing it, or generating a colliding one, directly breaks or ambiguates routing.

A hybrid ID+slug URL, the pattern used by sites like Stack Overflow (/questions/12345/some-descriptive-title), treats a numeric or short alphanumeric ID as the authoritative lookup key and the trailing slug as a cosmetic, SEO-oriented addition. The server resolves the resource from the ID alone and can safely ignore, or silently correct, a stale or mismatched slug segment. This removes the need for a redirect table entirely: an old slug embedded in a bookmarked link still resolves correctly because the ID never changes, even if the descriptive text after it no longer matches the current title.

The tradeoff is a less clean URL and a routing layer that has to parse out and validate the ID segment before touching the slug at all. For content that rarely renames itself (documentation pages, product listings), a pure slug is usually simpler to maintain. For user-generated or frequently retitled content (forum threads, ticketing systems), the hybrid pattern avoids building a full slug-history mechanism just to keep old links alive.

Slug Persistence and Redirect Strategy

For teams staying with a pure slug scheme, the redirect problem raised in the pitfalls above needs an actual implementation, not just a policy. The standard pattern is a slug history table separate from the main content table:

sql
CREATE TABLE slug_history (
    entity_id   BIGINT NOT NULL REFERENCES posts(id),
    old_slug    TEXT NOT NULL UNIQUE,
    replaced_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

On a slug change, the application writes the outgoing slug into slug_history before updating the live posts.slug column. A request for an unrecognized slug then falls back to a lookup against slug_history and issues a 301 redirect to the entity's current slug, rather than returning a 404.

One edge case this table needs to guard against: once an old slug is retired, should it become available for a completely unrelated new post to claim? Reusing it breaks every existing redirect pointing at the original entity, since the old_slug value would no longer map to a single entity. The safest default is to treat every slug that has ever been live, current or historical, as permanently reserved, enforced by the same UNIQUE constraint the history table already carries above.

Slug Generation Across Frameworks

Most web frameworks provide a starting point for slug generation rather than leaving it entirely to hand-written regex:

  • Django ships django.utils.text.slugify() in the standard library, and its SlugField model field pairs naturally with a pre_save signal or an overridden save() method to auto-populate the slug from a title field on first save.
  • Ruby on Rails projects commonly add the FriendlyId gem, which replaces the default to_param method (normally just the row's ID) with a slug column, and includes a built-in candidates API that automatically tries a sequence of fallback slugs on collision.
  • Node.js and most JavaScript meta-frameworks have no slugify function in the standard library. Teams typically pull in a small dependency (the slugify npm package is a common choice) and call it explicitly in a route handler, a build-time static generation function, or a database hook, since dynamic route segments are matched by exact, case-sensitive string equality by default.

Practical Comparison Table

RuleCompliant ExampleNon-Compliant Example
Character Setedge-computing-2026Edge_Computing_2026!
Word Separatorapi-rate-limitingapi_rate_limiting
Hyphen Placementzero-trust-architecture-zero-trust--architecture-
Length (SERP display)kubernetes-network-policiesA 120+ character slug truncated mid-word in search results
Non-ASCII Handlingcafe-integration-guideRaw accented slug rendered as an escaped byte sequence in some contexts
Uniqueness on Collisionwebsocket-scaling-2A second, unrelated page silently reusing websocket-scaling

Code Implementation

The following TypeScript function covers transliteration, character filtering, and hyphen normalization in one pass. It does not handle uniqueness, which is a database-layer concern (checking existing slugs and appending a numeric suffix on collision).

typescript
function slugify(input: string): string {
  return input
    .normalize("NFKD")                 // split accented characters into base + diacritic
    .replace(/[\u0300-\u036f]/g, "")   // strip the diacritic marks
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\s-]/g, "")       // drop anything outside the allowed character set
    .replace(/[\s_]+/g, "-")            // collapse whitespace and underscores to one hyphen
    .replace(/-+/g, "-")                // collapse repeated hyphens
    .replace(/^-|-$/g, "");             // trim leading or trailing hyphens
}

slugify("Cafe Integration Guide!"); // "cafe-integration-guide"
slugify("  What Is a URL Slug?  "); // "what-is-a-url-slug"

The collision-resolution step referenced above is a small wrapper around whatever lookup the datastore already exposes:

typescript
async function getUniqueSlug(
  base: string,
  exists: (candidate: string) => Promise<boolean>
): Promise<string> {
  let candidate = base;
  let suffix = 2;

  while (await exists(candidate)) {
    candidate = `${base}-${suffix}`;
    suffix++;
  }

  return candidate;
}

// exists() wraps a single indexed lookup against both the live slug
// column and the slug_history table, e.g.:
// SELECT 1 FROM posts WHERE slug = $1
// UNION SELECT 1 FROM slug_history WHERE old_slug = $1 LIMIT 1;

Checking slug_history in the same lookup is what keeps a retired slug from being reissued to an unrelated new post, closing the edge case raised in the persistence section above.

Conclusion & Next Steps

A valid slug is less about what RFC 3986 technically permits and more about the narrower convention that keeps URLs readable, predictable, and safe to type by hand: lowercase letters, digits, single hyphens, transliterated non-ASCII input, and a uniqueness check that survives collisions without silently overwriting existing content. Reserved-word checks, a slug history table for redirects, and a considered choice between a pure slug and a hybrid ID+slug scheme close the remaining gap between a working slugify function and a routing layer that will not break in production.

---

Test URL Encoding Behavior with Yurlie

See exactly how a raw title becomes a browser-safe URL, character by character, with our free Yurlie URL Percent Encoder and Decoder.

Developer Suite

Yurlie Online Developer Tools

30 privacy-first utilities for developers and cloud architects

UUIDs, hashing, regex, timestamps, SQL, and networking utilities running 100% in your browser.

Explore All 30 Tools →
Total Views: 3Category: developer