feat: add SEO metadata, llms.txt, and markdown page mirrors
Centralize site metadata and social URLs for JSON-LD, expose llmstxt.org content and per-page .md routes for LLM crawlers, and refactor blog/FAQ/events pages with shared components. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import EventDetailClient from "./EventDetailClient";
|
||||
import { EventJsonLd, BreadcrumbJsonLd } from "@/components/public/JsonLd";
|
||||
import { apiUrl } from "@/lib/api-base";
|
||||
import { getMeetupStartUtc, getMeetupEndUtc } from "@/lib/meetupEventTime";
|
||||
|
||||
async function fetchEvent(id: string) {
|
||||
try {
|
||||
@@ -59,6 +60,15 @@ export default async function EventDetailPage({ params }: Props) {
|
||||
const event = await fetchEvent(id);
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://belgianbitcoinembassy.org";
|
||||
|
||||
let startDate = event?.date;
|
||||
let endDate: string | undefined;
|
||||
if (event?.date) {
|
||||
const start = getMeetupStartUtc(event.date, event.time || "00:00");
|
||||
if (!Number.isNaN(start.getTime())) startDate = start.toISOString();
|
||||
const end = getMeetupEndUtc(event.date, event.time || "");
|
||||
if (end && !Number.isNaN(end.getTime())) endDate = end.toISOString();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{event && (
|
||||
@@ -66,7 +76,8 @@ export default async function EventDetailPage({ params }: Props) {
|
||||
<EventJsonLd
|
||||
name={event.title}
|
||||
description={event.description}
|
||||
startDate={event.date}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
location={event.location}
|
||||
url={`${siteUrl}/events/${id}`}
|
||||
imageUrl={event.imageId ? `${siteUrl}/media/${event.imageId}` : undefined}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { BreadcrumbJsonLd } from "@/components/public/JsonLd";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Events - Bitcoin Meetups in Belgium",
|
||||
@@ -13,5 +14,15 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default function EventsLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
return (
|
||||
<>
|
||||
<BreadcrumbJsonLd
|
||||
items={[
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Events", href: "/events" },
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,46 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { getMeetupStartUtc } from "@/lib/meetupEventTime";
|
||||
import { Navbar } from "@/components/public/Navbar";
|
||||
import { Footer } from "@/components/public/Footer";
|
||||
import { MeetupCard } from "@/components/public/MeetupCard";
|
||||
import { AddToCalendarButton } from "@/components/public/AddToCalendarDialog";
|
||||
import { getMeetupStartUtc } from "@/lib/meetupEventTime";
|
||||
import { apiUrl } from "@/lib/api-base";
|
||||
|
||||
function CardSkeleton() {
|
||||
return (
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 animate-pulse">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="bg-zinc-800 rounded-lg w-[52px] h-[58px] shrink-0" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 bg-zinc-800 rounded w-3/4" />
|
||||
<div className="h-3 bg-zinc-800 rounded w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="h-3 bg-zinc-800 rounded w-full" />
|
||||
<div className="h-3 bg-zinc-800 rounded w-5/6" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// Re-render at most every 5 minutes so the upcoming/past split stays current.
|
||||
export const revalidate = 300;
|
||||
|
||||
async function fetchMeetups(): Promise<any[]> {
|
||||
try {
|
||||
const res = await fetch(apiUrl("/meetups"), { next: { revalidate: 300 } });
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default function EventsPage() {
|
||||
const [meetups, setMeetups] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.getMeetups()
|
||||
.then((data: any) => {
|
||||
const list = Array.isArray(data) ? data : [];
|
||||
setMeetups(list);
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
export default async function EventsPage() {
|
||||
const meetups = await fetchMeetups();
|
||||
|
||||
const now = new Date();
|
||||
const upcoming = meetups.filter((m) => {
|
||||
@@ -75,17 +55,11 @@ export default function EventsPage() {
|
||||
</header>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-8 pb-24 space-y-20">
|
||||
{error && (
|
||||
<div className="bg-red-900/20 text-red-400 rounded-xl p-6 text-sm">
|
||||
Failed to load events: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-xl font-black flex items-center gap-3">
|
||||
Upcoming
|
||||
{!loading && upcoming.length > 0 && (
|
||||
{upcoming.length > 0 && (
|
||||
<span className="text-xs font-bold bg-primary/10 text-primary px-2.5 py-1 rounded-full">
|
||||
{upcoming.length}
|
||||
</span>
|
||||
@@ -94,11 +68,7 @@ export default function EventsPage() {
|
||||
<AddToCalendarButton />
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{[0, 1, 2].map((i) => <CardSkeleton key={i} />)}
|
||||
</div>
|
||||
) : upcoming.length === 0 ? (
|
||||
{upcoming.length === 0 ? (
|
||||
<div className="border border-zinc-800/60 rounded-xl px-8 py-12 text-center">
|
||||
<p className="text-on-surface-variant text-sm">
|
||||
No upcoming events scheduled. Check back soon.
|
||||
@@ -106,26 +76,23 @@ export default function EventsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{upcoming.map((m) => <MeetupCard key={m.id} meetup={m} />)}
|
||||
{upcoming.map((m) => (
|
||||
<MeetupCard key={m.id} meetup={m} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(loading || past.length > 0) && (
|
||||
{past.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-black mb-8 text-on-surface-variant/60">
|
||||
Past Events
|
||||
</h2>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{[0, 1, 2].map((i) => <CardSkeleton key={i} />)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{past.map((m) => <MeetupCard key={m.id} meetup={m} muted />)}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{past.map((m) => (
|
||||
<MeetupCard key={m.id} meetup={m} muted />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user