Slug Uniqueness Strategies Guide

May 28, 20266 min read

Every page on your site needs a unique URL slug. When two articles share the same title — "Introduction to Python" appears in both a beginner series and a data science track — you need a strategy to disambiguate them. The approach you choose affects URL aesthetics, SEO value, and system complexity. This guide covers three proven strategies with their tradeoffs.

The Problem: Title Collisions

Title collisions are more common than you might expect:

  • Series articles: "Part 1", "Part 2" → identical after slug truncation
  • Updated posts: "React Tutorial" reposted in 2025 and 2026
  • Category overlaps: same topic covered in different contexts
  • User-generated content: multiple users naming items identically
// Collision example
slugify('Introduction to Python')       // 'introduction-to-python'
slugify('Introduction to Python')       // 'introduction-to-python' — duplicate!

Without a uniqueness strategy, the second article either overwrites the first or fails to save — both are bad outcomes.

Strategy 1: Numeric Suffixes

The simplest approach adds an incrementing number when a collision occurs:

async function uniqueNumericSlug(baseSlug, slugExists) {
  if (!await slugExists(baseSlug)) return baseSlug

  let counter = 2
  while (await slugExists(`${baseSlug}-${counter}`)) {
    counter++
  }
  return `${baseSlug}-${counter}`
}

// Results:
// 'introduction-to-python'
// 'introduction-to-python-2'
// 'introduction-to-python-3'

Advantages:

  • URLs remain human-readable
  • Sequential numbering feels natural
  • No external dependencies

Disadvantages:

  • Numbers carry no semantic meaning — -2 tells you nothing about content difference
  • Gaps appear when articles are deleted (1, 2, 4 if 3 is removed)
  • Race conditions in concurrent environments — two requests might both check for introduction-to-python-2 at the same time and both claim it

Race condition fix: Use a database unique constraint and retry on conflict:

async function safeNumericSlug(baseSlug, saveSlug) {
  let slug = baseSlug
  let counter = 1

  while (true) {
    try {
      await saveSlug(slug)  // INSERT with unique constraint
      return slug
    } catch (error) {
      if (error.code !== 'UNIQUE_CONSTRAINT') throw error
      counter++
      slug = `${baseSlug}-${counter}`
    }
  }
}

Strategy 2: UUID or Hash Appends

Append a short unique identifier to guarantee uniqueness without database queries:

function uuidSlug(baseSlug) {
  const id = crypto.randomUUID().slice(0, 8)  // First 8 chars of UUID v4
  return `${baseSlug}-${id}`
}

// Results:
// 'introduction-to-python-a3f1c8e7'
// 'introduction-to-python-7b2d9f41'

Advantages:

  • No database query needed — uniqueness is probabilistically guaranteed
  • No race conditions
  • No sequential numbering gaps
  • Short IDs (8 hex chars = 4 billion combinations) are sufficient for collision avoidance

Disadvantages:

  • URLs are longer and less memorable
  • The random suffix carries no meaning
  • Aesthetically less clean than numeric suffixes

Collision probability: For 8 hex characters (32 bits), the birthday problem gives a 50% collision chance at roughly 65,536 slugs with the same base. For most sites, this is more than adequate. If you need stronger guarantees, use 12 characters (48 bits, 50% collision at ~16 million).

A compromise variant uses a content-based hash instead of random:

function hashSlug(baseSlug, content) {
  const hash = hashString(content).toString(36).slice(0, 6)
  return `${baseSlug}-${hash}`
}

// Same title + same content → same slug (idempotent)
// Same title + different content → different slug (disambiguated)

This makes the slug deterministic — the same article always gets the same slug, which is useful for content migration and CI/CD pipelines.

Strategy 3: Database-Driven Uniqueness

Store slugs in a dedicated table with controlled allocation:

CREATE TABLE slugs (
  slug VARCHAR(255) PRIMARY KEY,
  entity_type VARCHAR(50) NOT NULL,
  entity_id BIGINT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_slugs_entity ON slugs(entity_type, entity_id);
async function allocateSlug(baseSlug, entityType, entityId, pool) {
  let slug = baseSlug
  let counter = 1

  while (true) {
    try {
      await pool.query(
        'INSERT INTO slugs (slug, entity_type, entity_id) VALUES ($1, $2, $3)',
        [slug, entityType, entityId]
      )
      return slug
    } catch (error) {
      if (error.code !== '23505') throw error  // unique_violation
      counter++
      slug = `${baseSlug}-${counter}`
    }
  }
}

Advantages:

  • Central slug registry enables redirects when slugs change
  • Supports slug history — old slugs can 301-redirect to current ones
  • Entity association enables bulk operations (e.g., redirect all slugs for a deleted article)

Disadvantages:

  • Additional database table and queries
  • More complex migration and backup considerations
  • Overkill for small sites

Slug History and Redirects

When an article's title changes, its slug should change too — but the old slug must still work. A slug history table solves this:

CREATE TABLE slug_history (
  old_slug VARCHAR(255) PRIMARY KEY,
  new_slug VARCHAR(255) NOT NULL,
  redirected_at TIMESTAMP DEFAULT NOW()
);
async function updateSlug(entityType, entityId, newSlug, pool) {
  const oldSlug = await getCurrentSlug(entityType, entityId, pool)
  if (oldSlug === newSlug) return

  await pool.query('BEGIN')
  try {
    await pool.query(
      'UPDATE slugs SET slug = $1 WHERE entity_type = $2 AND entity_id = $3',
      [newSlug, entityType, entityId]
    )
    await pool.query(
      'INSERT INTO slug_history (old_slug, new_slug) VALUES ($1, $2)',
      [oldSlug, newSlug]
    )
    await pool.query('COMMIT')
  } catch (error) {
    await pool.query('ROLLBACK')
    throw error
  }
}

When a request comes in for the old slug, check slug_history and respond with a 301 redirect to the new one. This preserves SEO equity — search engines transfer link authority through 301s.

Choosing a Strategy

Site SizeRecommended Strategy
Blog (< 100 posts)Numeric suffixes
Medium (< 10K pages)Hash appends
Large (10K+ pages)Database-driven with history
User-generated contentUUID appends (concurrency-safe)

Generate unique slugs instantly at /tools/slug-generator, which handles collision detection and supports numeric suffix, hash, and custom suffix modes for any content management workflow.