🎯 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! 🚀
66 lines
No EOL
1.6 KiB
JavaScript
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
|
|
};
|
|
}
|