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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user