Files
Spanglish/frontend/src/app/(public)/events/[id]/page.tsx
root 47ba754f05 Add full SEO optimization for Spanglish social and language events
- Add comprehensive metadata to root layout with Open Graph, Twitter cards
- Create dynamic sitemap.ts for all pages and events
- Create robots.ts with proper allow/disallow rules
- Add JSON-LD Event structured data to event detail pages
- Add page-specific metadata to events, community, contact, FAQ pages
- Add FAQ structured data schema
- Update footer with local SEO text for Asunción, Paraguay
- Add web manifest for mobile SEO
- Create 404 page with proper noindex
- Optimize image alt text and add lazy loading
- Add NEXT_PUBLIC_SITE_URL env variable
- Add about/ folder to gitignore
2026-01-30 21:05:25 +00:00

148 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import EventDetailClient from './EventDetailClient';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://spanglish.com.py';
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
interface Event {
id: string;
title: string;
titleEs?: string;
description: string;
descriptionEs?: string;
startDatetime: string;
endDatetime?: string;
location: string;
locationUrl?: string;
price: number;
currency: string;
capacity: number;
status: 'draft' | 'published' | 'cancelled' | 'completed' | 'archived';
bannerUrl?: string;
availableSeats?: number;
bookedCount?: number;
createdAt: string;
updatedAt: string;
}
async function getEvent(id: string): Promise<Event | null> {
try {
const response = await fetch(`${apiUrl}/api/events/${id}`, {
next: { revalidate: 60 },
});
if (!response.ok) return null;
const data = await response.json();
return data.event || null;
} catch {
return null;
}
}
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const event = await getEvent(params.id);
if (!event) {
return { title: 'Event Not Found' };
}
const eventDate = new Date(event.startDatetime).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
const title = `${event.title} English & Spanish Meetup in Asunción`;
const description = `Join Spanglish on ${eventDate} in Asunción. Practice English and Spanish in a relaxed social setting. Limited spots available.`;
return {
title,
description,
openGraph: {
title,
description,
type: 'website',
url: `${siteUrl}/events/${event.id}`,
images: event.bannerUrl
? [{ url: event.bannerUrl, width: 1200, height: 630, alt: event.title }]
: [{ url: `${siteUrl}/images/og-image.jpg`, width: 1200, height: 630, alt: 'Spanglish Language Exchange Event' }],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: event.bannerUrl ? [event.bannerUrl] : [`${siteUrl}/images/og-image.jpg`],
},
alternates: {
canonical: `${siteUrl}/events/${event.id}`,
},
};
}
function generateEventJsonLd(event: Event) {
const isPastEvent = new Date(event.startDatetime) < new Date();
const isCancelled = event.status === 'cancelled';
return {
'@context': 'https://schema.org',
'@type': 'Event',
name: event.title,
description: event.description,
startDate: event.startDatetime,
endDate: event.endDatetime || event.startDatetime,
eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',
eventStatus: isCancelled
? 'https://schema.org/EventCancelled'
: isPastEvent
? 'https://schema.org/EventPostponed'
: 'https://schema.org/EventScheduled',
location: {
'@type': 'Place',
name: event.location,
address: {
'@type': 'PostalAddress',
addressLocality: 'Asunción',
addressCountry: 'PY',
},
},
organizer: {
'@type': 'Organization',
name: 'Spanglish',
url: siteUrl,
},
offers: {
'@type': 'Offer',
price: event.price,
priceCurrency: event.currency,
availability: event.availableSeats && event.availableSeats > 0
? 'https://schema.org/InStock'
: 'https://schema.org/SoldOut',
url: `${siteUrl}/events/${event.id}`,
validFrom: new Date().toISOString(),
},
image: event.bannerUrl || `${siteUrl}/images/og-image.jpg`,
url: `${siteUrl}/events/${event.id}`,
};
}
export default async function EventDetailPage({ params }: { params: { id: string } }) {
const event = await getEvent(params.id);
if (!event) {
notFound();
}
const jsonLd = generateEventJsonLd(event);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<EventDetailClient eventId={params.id} initialEvent={event} />
</>
);
}