Slug Uniqueness Strategies Guide
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 —
-2tells 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-2at 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 Size | Recommended Strategy |
|---|---|
| Blog (< 100 posts) | Numeric suffixes |
| Medium (< 10K pages) | Hash appends |
| Large (10K+ pages) | Database-driven with history |
| User-generated content | UUID 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.