/** * Utility functions for generating and parsing SEO-friendly URL slugs * * Slug format: {title}-{location}-{short-id} * Example: italian-restaurant-austin-tx-a3f7b2c1 */ /** * Generate a SEO-friendly URL slug from listing data * * @param title - The listing title (e.g., "Italian Restaurant") * @param location - Location object with name, county, and state * @param id - The listing UUID * @returns SEO-friendly slug (e.g., "italian-restaurant-austin-tx-a3f7b2c1") */ export function generateSlug(title: string, location: any, id: string): string { if (!title || !id) { throw new Error('Title and ID are required to generate a slug'); } // Clean and slugify the title const titleSlug = title .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') // Remove special characters .replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen .substring(0, 50); // Limit title to 50 characters // Get location string let locationSlug = ''; if (location) { const locationName = location.name || location.county || ''; const state = location.state || ''; if (locationName) { locationSlug = locationName .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-'); } if (state) { locationSlug = locationSlug ? `${locationSlug}-${state.toLowerCase()}` : state.toLowerCase(); } } // Get first 8 characters of UUID for uniqueness const shortId = id.substring(0, 8); // Combine parts: title-location-id const parts = [titleSlug, locationSlug, shortId].filter(Boolean); const slug = parts.join('-'); // Final cleanup return slug .replace(/-+/g, '-') // Remove duplicate hyphens .replace(/^-|-$/g, '') // Remove leading/trailing hyphens .toLowerCase(); } /** * Extract the UUID from a slug * The UUID is always the last segment (8 characters) * * @param slug - The URL slug (e.g., "italian-restaurant-austin-tx-a3f7b2c1") * @returns The short ID (e.g., "a3f7b2c1") */ export function extractShortIdFromSlug(slug: string): string { if (!slug) { throw new Error('Slug is required'); } const parts = slug.split('-'); return parts[parts.length - 1]; } /** * Validate if a string looks like a valid slug * * @param slug - The string to validate * @returns true if the string looks like a valid slug */ export function isValidSlug(slug: string): boolean { if (!slug || typeof slug !== 'string') { return false; } // Check if slug contains only lowercase letters, numbers, and hyphens const slugPattern = /^[a-z0-9-]+$/; if (!slugPattern.test(slug)) { return false; } // Check if slug has a reasonable length (at least 10 chars for short-id + some content) if (slug.length < 10) { return false; } // Check if last segment looks like a UUID prefix (8 chars of alphanumeric) const parts = slug.split('-'); const lastPart = parts[parts.length - 1]; return lastPart.length === 8 && /^[a-z0-9]{8}$/.test(lastPart); } /** * Check if a parameter is a slug (vs a UUID) * * @param param - The URL parameter * @returns true if it's a slug, false if it's likely a UUID */ export function isSlug(param: string): boolean { if (!param) { return false; } // UUIDs have a specific format with hyphens at specific positions // e.g., "a3f7b2c1-4d5e-6789-abcd-1234567890ef" const uuidPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; if (uuidPattern.test(param)) { return false; // It's a UUID } // If it contains at least 3 parts (e.g., title-state-id or title-city-state-id) and looks like our slug format, it's probably a slug return param.split('-').length >= 3 && isValidSlug(param); } /** * Regenerate slug from updated listing data * Useful when title or location changes * * @param title - Updated title * @param location - Updated location * @param existingSlug - The current slug (to preserve short-id) * @returns New slug with same short-id */ export function regenerateSlug(title: string, location: any, existingSlug: string): string { if (!existingSlug) { throw new Error('Existing slug is required to regenerate'); } const shortId = extractShortIdFromSlug(existingSlug); // Reconstruct full UUID from short-id (not possible, so we use full existing slug's ID) // In practice, you'd need the full UUID from the database // For now, we'll construct a new slug with the short-id const titleSlug = title .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .substring(0, 50); let locationSlug = ''; if (location) { const locationName = location.name || location.county || ''; const state = location.state || ''; if (locationName) { locationSlug = locationName .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-'); } if (state) { locationSlug = locationSlug ? `${locationSlug}-${state.toLowerCase()}` : state.toLowerCase(); } } const parts = [titleSlug, locationSlug, shortId].filter(Boolean); return parts.join('-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase(); }