deckhearth/lib/slug-utils.js
Randall Stillwell 50a3156b92 🔗 Implement Collection Slug URLs
🎯 Vanity URLs for Collections:
- Added slug-based URLs like /collection/modern-masters-2021
- Backwards compatible with numeric IDs
- SEO-friendly and memorable URLs

🛠️ Slug System:
- Created lib/slug-utils.js with slug generation and validation
- generateSlug() converts names to URL-friendly format
- generateUniqueSlug() handles duplicates with numeric suffixes
- isValidSlug() validates format (lowercase, hyphens, no special chars)

📊 Database Schema:
- Added slug column to collections table with unique constraint
- Migration script adds slugs to existing collections
- Database constraints ensure slug format and uniqueness
- Performance index on slug column

🔌 API Updates:
- Updated collections API to generate slugs for new collections
- New [identifier].js endpoint handles both slugs and IDs
- Thumbnails API supports both slug and ID lookups
- Smart identifier detection (slug vs numeric ID)

🎨 Frontend Integration:
- Collections page uses slugs for navigation
- Fallback to ID if slug not available (backwards compatibility)
- Updated all collection links to use slugs
- Sample collections created with proper slugs

 URL Examples:
- /collection/modern-masters-2021 (new slug format)
- /collection/123 (old ID format still works)
- Automatic redirect potential for future

The collection URLs are now beautiful and shareable! 🚀
2025-07-26 22:06:52 -05:00

66 lines
No EOL
1.6 KiB
JavaScript

/**
* Generate a URL-friendly slug from a collection name
*/
export function generateSlug(name) {
return name
.toLowerCase()
.trim()
// Replace spaces and special characters with hyphens
.replace(/[^a-z0-9]+/g, '-')
// Remove leading/trailing hyphens
.replace(/^-+|-+$/g, '')
// Limit length to 50 characters
.substring(0, 50)
// Remove trailing hyphen if truncation created one
.replace(/-+$/, '');
}
/**
* Generate a unique slug by checking against existing slugs
*/
export async function generateUniqueSlug(name, existingSlugs = []) {
const baseSlug = generateSlug(name);
// If base slug is unique, use it
if (!existingSlugs.includes(baseSlug)) {
return baseSlug;
}
// Find the next available number suffix
let counter = 2;
let uniqueSlug = `${baseSlug}-${counter}`;
while (existingSlugs.includes(uniqueSlug)) {
counter++;
uniqueSlug = `${baseSlug}-${counter}`;
}
return uniqueSlug;
}
/**
* Validate a slug format
*/
export function isValidSlug(slug) {
if (!slug || typeof slug !== 'string') {
return false;
}
// Must be 1-50 characters, lowercase letters, numbers, and hyphens only
// Cannot start or end with hyphen
const slugRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
return slugRegex.test(slug) && slug.length <= 50;
}
/**
* Convert existing collection names to suggested slugs for migration
*/
export function suggestSlugForCollection(collection) {
const baseSlug = generateSlug(collection.name);
return {
id: collection.id,
name: collection.name,
currentSlug: collection.slug || null,
suggestedSlug: baseSlug
};
}