Add human-readable event URL slugs with legacy redirect support.
Store unique slugs on events, backfill existing records, redirect old UUID and alias URLs to canonical slug pages, and expose slug editing plus alias management in the admin event modal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -166,7 +166,7 @@ export default function BookingPage() {
|
||||
const soldOut = bookedCount >= capacity;
|
||||
if (soldOut) {
|
||||
toast.error(t('events.details.soldOut'));
|
||||
router.push(`/events/${eventRes.event.id}`);
|
||||
router.push(`/events/${eventRes.event.slug}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1049,7 +1049,7 @@ export default function BookingPage() {
|
||||
<div className="section-padding bg-secondary-gray min-h-screen">
|
||||
<div className="container-page max-w-2xl">
|
||||
<Link
|
||||
href={`/events/${event.id}`}
|
||||
href={`/events/${event.slug}`}
|
||||
className="inline-flex items-center gap-2 text-gray-600 hover:text-primary-dark mb-6"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function NextEventSection({ initialEvent }: NextEventSectionProps
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={`/events/${nextEvent.id}`} className="block group">
|
||||
<Link href={`/events/${nextEvent.slug}`} className="block group">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-2xl overflow-hidden shadow-lg transition-all duration-300 hover:shadow-2xl hover:scale-[1.01]">
|
||||
<div className="flex flex-col md:flex-row">
|
||||
{/* Banner */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { notFound, permanentRedirect } from 'next/navigation';
|
||||
import EventDetailClient from './EventDetailClient';
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://spanglish.com.py';
|
||||
@@ -7,6 +7,7 @@ const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
titleEs?: string;
|
||||
description: string;
|
||||
@@ -68,7 +69,7 @@ export async function generateMetadata({ params }: { params: { id: string } }):
|
||||
title,
|
||||
description,
|
||||
type: 'website',
|
||||
url: `${siteUrl}/events/${event.id}`,
|
||||
url: `${siteUrl}/events/${event.slug}`,
|
||||
images: [{ url: imageUrl, width: 1200, height: 630, alt: event.title }],
|
||||
},
|
||||
twitter: {
|
||||
@@ -78,7 +79,7 @@ export async function generateMetadata({ params }: { params: { id: string } }):
|
||||
images: [imageUrl],
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${siteUrl}/events/${event.id}`,
|
||||
canonical: `${siteUrl}/events/${event.slug}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -119,11 +120,11 @@ function generateEventJsonLd(event: Event) {
|
||||
availability: Math.max(0, (event.capacity ?? 0) - (event.bookedCount ?? 0)) > 0
|
||||
? 'https://schema.org/InStock'
|
||||
: 'https://schema.org/SoldOut',
|
||||
url: `${siteUrl}/events/${event.id}`,
|
||||
url: `${siteUrl}/events/${event.slug}`,
|
||||
validFrom: new Date().toISOString(),
|
||||
},
|
||||
image: event.bannerUrl || `${siteUrl}/images/og-image.jpg`,
|
||||
url: `${siteUrl}/events/${event.id}`,
|
||||
url: `${siteUrl}/events/${event.slug}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,6 +135,11 @@ export default async function EventDetailPage({ params }: { params: { id: string
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Redirect legacy UUID/alias URLs to the canonical slug (HTTP 308 permanent)
|
||||
if (event.slug && params.id !== event.slug) {
|
||||
permanentRedirect(`/events/${event.slug}`);
|
||||
}
|
||||
|
||||
const jsonLd = generateEventJsonLd(event);
|
||||
|
||||
return (
|
||||
@@ -142,7 +148,7 @@ export default async function EventDetailPage({ params }: { params: { id: string
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<EventDetailClient eventId={params.id} initialEvent={event} />
|
||||
<EventDetailClient eventId={event.slug} initialEvent={event} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function EventsPage() {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{displayedEvents.map((event) => (
|
||||
<Link key={event.id} href={`/events/${event.id}`} className="block">
|
||||
<Link key={event.id} href={`/events/${event.slug}`} className="block">
|
||||
<Card variant="elevated" className="card-hover overflow-hidden cursor-pointer h-full">
|
||||
{/* Event banner */}
|
||||
{event.bannerUrl ? (
|
||||
|
||||
@@ -13,6 +13,7 @@ const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
interface NextEvent {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
titleEs?: string;
|
||||
description: string;
|
||||
@@ -139,10 +140,10 @@ function generateNextEventJsonLd(event: NextEvent) {
|
||||
(event.availableSeats ?? 0) > 0
|
||||
? 'https://schema.org/InStock'
|
||||
: 'https://schema.org/SoldOut',
|
||||
url: `${siteUrl}/events/${event.id}`,
|
||||
url: `${siteUrl}/events/${event.slug}`,
|
||||
},
|
||||
image: event.bannerUrl || `${siteUrl}/images/og-image.jpg`,
|
||||
url: `${siteUrl}/events/${event.id}`,
|
||||
url: `${siteUrl}/events/${event.slug}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user