🎯 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! 🚀
124 lines
No EOL
4 KiB
JavaScript
124 lines
No EOL
4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import dotenv from 'dotenv';
|
|
import { neon } from '@neondatabase/serverless';
|
|
import { generateUniqueSlug } from '../lib/slug-utils.js';
|
|
|
|
dotenv.config({ path: '.env.local' });
|
|
|
|
async function addCollectionSlugs() {
|
|
const sql = neon(process.env.POSTGRES_URL);
|
|
|
|
try {
|
|
console.log('🔗 Adding slug support to collections...');
|
|
|
|
// Step 1: Add slug column to collections table
|
|
console.log('📝 Adding slug column...');
|
|
await sql`
|
|
ALTER TABLE collections
|
|
ADD COLUMN IF NOT EXISTS slug VARCHAR(100) UNIQUE
|
|
`;
|
|
console.log('✅ Added slug column');
|
|
|
|
// Step 2: Get all existing collections
|
|
console.log('🔍 Fetching existing collections...');
|
|
const collections = await sql`
|
|
SELECT id, name, slug FROM collections
|
|
ORDER BY created_at ASC
|
|
`;
|
|
console.log(`📊 Found ${collections.length} collections`);
|
|
|
|
if (collections.length === 0) {
|
|
console.log('✅ No collections to migrate');
|
|
return;
|
|
}
|
|
|
|
// Step 3: Generate unique slugs for all collections
|
|
console.log('🏷️ Generating unique slugs...');
|
|
const existingSlugs = [];
|
|
const updates = [];
|
|
|
|
for (const collection of collections) {
|
|
// Skip if collection already has a slug
|
|
if (collection.slug) {
|
|
existingSlugs.push(collection.slug);
|
|
console.log(`⏭️ Skipping "${collection.name}" - already has slug: ${collection.slug}`);
|
|
continue;
|
|
}
|
|
|
|
// Generate unique slug
|
|
const uniqueSlug = await generateUniqueSlug(collection.name, existingSlugs);
|
|
existingSlugs.push(uniqueSlug);
|
|
|
|
updates.push({
|
|
id: collection.id,
|
|
name: collection.name,
|
|
slug: uniqueSlug
|
|
});
|
|
|
|
console.log(`🎯 "${collection.name}" → "${uniqueSlug}"`);
|
|
}
|
|
|
|
// Step 4: Update collections with their new slugs
|
|
if (updates.length > 0) {
|
|
console.log(`📝 Updating ${updates.length} collections with slugs...`);
|
|
|
|
for (const update of updates) {
|
|
await sql`
|
|
UPDATE collections
|
|
SET slug = ${update.slug}
|
|
WHERE id = ${update.id}
|
|
`;
|
|
console.log(`✅ Updated: ${update.name} → ${update.slug}`);
|
|
}
|
|
}
|
|
|
|
// Step 5: Add index for performance
|
|
console.log('🚀 Adding slug index for performance...');
|
|
try {
|
|
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_collections_slug ON collections(slug)`;
|
|
console.log('✅ Added slug index');
|
|
} catch (error) {
|
|
if (!error.message.includes('already exists')) {
|
|
throw error;
|
|
}
|
|
console.log('✅ Slug index already exists');
|
|
}
|
|
|
|
// Step 6: Add constraint to ensure slugs are not null for new collections
|
|
console.log('🔒 Adding slug constraints...');
|
|
try {
|
|
await sql`ALTER TABLE collections ADD CONSTRAINT check_slug_format CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50)`;
|
|
console.log('✅ Added slug format constraint');
|
|
} catch (error) {
|
|
if (!error.message.includes('already exists')) {
|
|
console.warn('⚠️ Could not add slug constraint:', error.message);
|
|
} else {
|
|
console.log('✅ Slug constraint already exists');
|
|
}
|
|
}
|
|
|
|
console.log('\n🎉 Collection slug migration completed successfully!');
|
|
console.log('\n📋 Summary:');
|
|
console.log(` 📊 Total collections: ${collections.length}`);
|
|
console.log(` 🆕 New slugs created: ${updates.length}`);
|
|
console.log(` ⏭️ Already had slugs: ${collections.length - updates.length}`);
|
|
|
|
if (updates.length > 0) {
|
|
console.log('\n🔗 New URL structure:');
|
|
console.log(' Old: /collection/123');
|
|
console.log(' New: /collection/modern-masters-2021');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to add collection slugs:', error.message);
|
|
console.error('Full error:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
addCollectionSlugs();
|
|
}
|
|
|
|
export { addCollectionSlugs };
|