Files
BelgianBitcoinEmbassy/frontend/components/public/JsonLd.tsx
T
bbeandCursor ede8817583 fix: harden deploys and close top security holes after /events outage
Isolate next dev from production .next, add build-guard/atomic deploy/health
watchdog, error boundaries, and fix JWT startup, meetup leaks, media path
traversal, SVG/memory uploads, and JSON-LD escaping.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 19:14:57 +02:00

215 lines
5.2 KiB
TypeScript

interface JsonLdProps {
data: Record<string, unknown>;
}
export function JsonLd({ data }: JsonLdProps) {
// Escape `<` so user-controlled titles/descriptions cannot break out of the
// <script> tag via `</script>` (JSON.stringify alone does not escape it).
const json = JSON.stringify(data).replace(/</g, "\\u003c");
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: json }}
/>
);
}
const siteUrl =
process.env.NEXT_PUBLIC_SITE_URL || "https://belgianbitcoinembassy.org";
export function OrganizationJsonLd({ sameAs }: { sameAs?: string[] } = {}) {
const sameAsUrls =
sameAs && sameAs.length > 0 ? sameAs : ["https://t.me/belgianbitcoinembassy"];
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "Organization",
"@id": `${siteUrl}/#organization`,
name: "Belgian Bitcoin Embassy",
url: siteUrl,
logo: `${siteUrl}/og-default.png`,
description:
"Discover Bitcoin meetups across Belgium. Real conversations, education, and a strong local community.",
sameAs: sameAsUrls,
address: {
"@type": "PostalAddress",
addressLocality: "Antwerp",
addressCountry: "BE",
},
}}
/>
);
}
export function WebSiteJsonLd() {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "WebSite",
name: "Belgian Bitcoin Embassy",
url: siteUrl,
description:
"Discover Bitcoin meetups across Belgium. Real conversations, education, and a strong local community.",
publisher: {
"@type": "Organization",
name: "Belgian Bitcoin Embassy",
logo: { "@type": "ImageObject", url: `${siteUrl}/og-default.png` },
},
}}
/>
);
}
interface BlogPostingJsonLdProps {
title: string;
description: string;
slug: string;
publishedAt?: string;
authorName?: string;
}
export function BlogPostingJsonLd({
title,
description,
slug,
publishedAt,
authorName,
}: BlogPostingJsonLdProps) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: title,
description,
url: `${siteUrl}/blog/${slug}`,
...(publishedAt ? { datePublished: publishedAt } : {}),
author: {
"@type": "Person",
name: authorName || "Belgian Bitcoin Embassy",
},
publisher: {
"@type": "Organization",
name: "Belgian Bitcoin Embassy",
logo: { "@type": "ImageObject", url: `${siteUrl}/og-default.png` },
},
image: `${siteUrl}/og?title=${encodeURIComponent(title)}&type=blog`,
mainEntityOfPage: {
"@type": "WebPage",
"@id": `${siteUrl}/blog/${slug}`,
},
}}
/>
);
}
interface EventJsonLdProps {
name: string;
description?: string;
startDate: string;
endDate?: string;
location?: string;
url: string;
imageUrl?: string;
organizerName?: string;
organizerUrl?: string;
}
export function EventJsonLd({
name,
description,
startDate,
endDate,
location,
url,
imageUrl,
organizerName,
organizerUrl,
}: EventJsonLdProps) {
const orgName = organizerName || "Belgian Bitcoin Embassy";
const orgUrl = organizerUrl || siteUrl;
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "Event",
name,
description: description || `Bitcoin meetup: ${name}`,
startDate,
...(endDate ? { endDate } : {}),
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
eventStatus: "https://schema.org/EventScheduled",
...(location
? {
location: {
"@type": "Place",
name: location,
address: {
"@type": "PostalAddress",
addressLocality: location,
addressCountry: "BE",
},
},
}
: {}),
organizer: {
"@type": "Organization",
name: orgName,
url: orgUrl,
},
image:
imageUrl || `${siteUrl}/og?title=${encodeURIComponent(name)}&type=event`,
url,
}}
/>
);
}
interface FaqJsonLdProps {
items: { question: string; answer: string }[];
}
export function FaqPageJsonLd({ items }: FaqJsonLdProps) {
if (items.length === 0) return null;
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: items.map((item) => ({
"@type": "Question",
name: item.question,
acceptedAnswer: {
"@type": "Answer",
text: item.answer,
},
})),
}}
/>
);
}
interface BreadcrumbItem {
name: string;
href: string;
}
export function BreadcrumbJsonLd({ items }: { items: BreadcrumbItem[] }) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: `${siteUrl}${item.href}`,
})),
}}
/>
);
}