- Add organizer model/API, admin and public organizer pages, meetup cards - Refresh events/home/contact; add calendar dialog and carousel components - Optional Plausible via NEXT_PUBLIC_PLAUSIBLE_* env vars in root layout - Prisma migration, seed updates, baseline-and-migrate script Made-with: Cursor
93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
import type { Metadata } from "next";
|
|
import EventDetailClient from "./EventDetailClient";
|
|
import { EventJsonLd, BreadcrumbJsonLd } from "@/components/public/JsonLd";
|
|
import { apiUrl } from "@/lib/api-base";
|
|
|
|
async function fetchEvent(id: string) {
|
|
try {
|
|
const res = await fetch(apiUrl(`/meetups/${id}`), {
|
|
next: { revalidate: 300 },
|
|
});
|
|
if (!res.ok) return null;
|
|
return res.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
interface Props {
|
|
params: Promise<{ id: string }>;
|
|
}
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { id } = await params;
|
|
const event = await fetchEvent(id);
|
|
if (!event) {
|
|
return { title: "Event Not Found" };
|
|
}
|
|
|
|
const orgLabel = event.organizer?.name || "Belgian Bitcoin Embassy";
|
|
const description =
|
|
event.description?.slice(0, 160) ||
|
|
`Bitcoin meetup: ${event.title}${event.location ? ` in ${event.location}` : ""}. Organized by ${orgLabel}.`;
|
|
|
|
const ogImage = event.imageId
|
|
? `/media/${event.imageId}`
|
|
: `/og?title=${encodeURIComponent(event.title)}&type=event`;
|
|
|
|
return {
|
|
title: event.title,
|
|
description,
|
|
openGraph: {
|
|
type: "article",
|
|
title: event.title,
|
|
description,
|
|
images: [{ url: ogImage, width: 1200, height: 630, alt: event.title }],
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: event.title,
|
|
description,
|
|
images: [ogImage],
|
|
},
|
|
alternates: { canonical: `/events/${id}` },
|
|
};
|
|
}
|
|
|
|
export default async function EventDetailPage({ params }: Props) {
|
|
const { id } = await params;
|
|
const event = await fetchEvent(id);
|
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://belgianbitcoinembassy.org";
|
|
|
|
return (
|
|
<>
|
|
{event && (
|
|
<>
|
|
<EventJsonLd
|
|
name={event.title}
|
|
description={event.description}
|
|
startDate={event.date}
|
|
location={event.location}
|
|
url={`${siteUrl}/events/${id}`}
|
|
imageUrl={event.imageId ? `${siteUrl}/media/${event.imageId}` : undefined}
|
|
organizerName={event.organizer?.name}
|
|
organizerUrl={
|
|
event.organizer?.slug
|
|
? `${siteUrl}/events/organizer/${event.organizer.slug}`
|
|
: undefined
|
|
}
|
|
/>
|
|
<BreadcrumbJsonLd
|
|
items={[
|
|
{ name: "Home", href: "/" },
|
|
{ name: "Events", href: "/events" },
|
|
{ name: event.title, href: `/events/${id}` },
|
|
]}
|
|
/>
|
|
</>
|
|
)}
|
|
<EventDetailClient id={id} />
|
|
</>
|
|
);
|
|
}
|