171 lines
5.0 KiB
TypeScript
171 lines
5.0 KiB
TypeScript
import type { Metadata } from 'next';
|
||
import HeroSection from './components/HeroSection';
|
||
import NextEventSectionWrapper from './components/NextEventSectionWrapper';
|
||
import AboutSection from './components/AboutSection';
|
||
import MediaCarouselSection from './components/MediaCarouselSection';
|
||
import NewsletterSection from './components/NewsletterSection';
|
||
import HomepageFaqSection from './components/HomepageFaqSection';
|
||
import { getCarouselImages } from '@/lib/carouselImages';
|
||
|
||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://spanglish.com.py';
|
||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||
|
||
interface NextEvent {
|
||
id: string;
|
||
title: string;
|
||
titleEs?: string;
|
||
description: string;
|
||
descriptionEs?: string;
|
||
shortDescription?: string;
|
||
shortDescriptionEs?: string;
|
||
startDatetime: string;
|
||
endDatetime?: string;
|
||
location: string;
|
||
locationUrl?: string;
|
||
price: number;
|
||
currency: string;
|
||
capacity: number;
|
||
status: 'draft' | 'published' | 'cancelled' | 'completed' | 'archived';
|
||
bannerUrl?: string;
|
||
externalBookingEnabled?: boolean;
|
||
externalBookingUrl?: string;
|
||
availableSeats?: number;
|
||
bookedCount?: number;
|
||
isFeatured?: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
async function getNextUpcomingEvent(): Promise<NextEvent | null> {
|
||
try {
|
||
const revalidateSeconds =
|
||
parseInt(process.env.NEXT_EVENT_REVALIDATE_SECONDS || '3600', 10) || 3600;
|
||
const response = await fetch(`${apiUrl}/api/events/next/upcoming`, {
|
||
next: { tags: ['next-event'], revalidate: revalidateSeconds },
|
||
});
|
||
if (!response.ok) return null;
|
||
const data = await response.json();
|
||
return data.event || null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Dynamic metadata with next event date for AI crawlers and SEO
|
||
export async function generateMetadata(): Promise<Metadata> {
|
||
const event = await getNextUpcomingEvent();
|
||
|
||
if (!event) {
|
||
return {
|
||
title: 'Spanglish – Language Exchange Events in Asunción',
|
||
description:
|
||
'Practice English and Spanish at relaxed social events in Asunción. Meet locals and internationals. Join the next Spanglish meetup.',
|
||
};
|
||
}
|
||
|
||
const eventDate = new Date(event.startDatetime).toLocaleDateString('en-US', {
|
||
weekday: 'long',
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
timeZone: 'America/Asuncion',
|
||
});
|
||
|
||
const description = `Next event: ${eventDate} – ${event.title}. Practice English and Spanish at relaxed social events in Asunción. Meet locals and internationals.`;
|
||
|
||
return {
|
||
title: 'Spanglish – Language Exchange Events in Asunción',
|
||
description,
|
||
openGraph: {
|
||
title: 'Spanglish – Language Exchange Events in Asunción',
|
||
description,
|
||
url: siteUrl,
|
||
siteName: 'Spanglish',
|
||
type: 'website',
|
||
images: [
|
||
{
|
||
url: event.bannerUrl
|
||
? event.bannerUrl.startsWith('http')
|
||
? event.bannerUrl
|
||
: `${siteUrl}${event.bannerUrl}`
|
||
: `${siteUrl}/images/og-image.jpg`,
|
||
width: 1200,
|
||
height: 630,
|
||
alt: `Spanglish – ${event.title}`,
|
||
},
|
||
],
|
||
},
|
||
twitter: {
|
||
card: 'summary_large_image',
|
||
title: 'Spanglish – Language Exchange Events in Asunción',
|
||
description,
|
||
},
|
||
};
|
||
}
|
||
|
||
function generateNextEventJsonLd(event: NextEvent) {
|
||
return {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'Event',
|
||
name: event.title,
|
||
description: event.shortDescription || event.description,
|
||
startDate: event.startDatetime,
|
||
endDate: event.endDatetime || event.startDatetime,
|
||
eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',
|
||
eventStatus:
|
||
event.status === 'cancelled'
|
||
? 'https://schema.org/EventCancelled'
|
||
: '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 ?? 0) > 0
|
||
? 'https://schema.org/InStock'
|
||
: 'https://schema.org/SoldOut',
|
||
url: `${siteUrl}/events/${event.id}`,
|
||
},
|
||
image: event.bannerUrl || `${siteUrl}/images/og-image.jpg`,
|
||
url: `${siteUrl}/events/${event.id}`,
|
||
};
|
||
}
|
||
|
||
export default async function HomePage() {
|
||
const carouselImages = getCarouselImages();
|
||
const nextEvent = await getNextUpcomingEvent();
|
||
|
||
return (
|
||
<>
|
||
{nextEvent && (
|
||
<script
|
||
type="application/ld+json"
|
||
dangerouslySetInnerHTML={{
|
||
__html: JSON.stringify(generateNextEventJsonLd(nextEvent)),
|
||
}}
|
||
/>
|
||
)}
|
||
<HeroSection />
|
||
<NextEventSectionWrapper initialEvent={nextEvent} />
|
||
<AboutSection />
|
||
<MediaCarouselSection images={carouselImages} />
|
||
<NewsletterSection />
|
||
<HomepageFaqSection />
|
||
</>
|
||
);
|
||
}
|