From 8ad65be847712e90b5480fa9e7c67a033f32209c Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:51:33 +0200 Subject: [PATCH] =?UTF-8?q?restore:=20reports=20pages/nav/sitemap=20droppe?= =?UTF-8?q?d=20in=20dev=E2=86=90main=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/reports/[category]/[slug]/page.tsx | 432 ++++++++++++++++++ src/app/reports/[category]/page.tsx | 119 +++++ src/app/reports/[category]/rss.xml/route.ts | 71 +++ src/app/reports/page.tsx | 146 ++++++ src/app/reports/rss.xml/route.ts | 66 +++ src/components/reports/region-winners.tsx | 155 +++++++ src/components/reports/report-card.tsx | 119 +++++ src/components/reports/stat-table.tsx | 167 +++++++ src/components/site-footer.tsx | 1 + src/components/site-header.tsx | 5 + .../rpc/2026-08-state-of-public-rpc.mdx | 200 ++++++++ src/lib/reports/loader.ts | 105 +++++ src/lib/sitemap-builder.ts | 34 ++ 13 files changed, 1620 insertions(+) create mode 100644 src/app/reports/[category]/[slug]/page.tsx create mode 100644 src/app/reports/[category]/page.tsx create mode 100644 src/app/reports/[category]/rss.xml/route.ts create mode 100644 src/app/reports/page.tsx create mode 100644 src/app/reports/rss.xml/route.ts create mode 100644 src/components/reports/region-winners.tsx create mode 100644 src/components/reports/report-card.tsx create mode 100644 src/components/reports/stat-table.tsx create mode 100644 src/content/reports/rpc/2026-08-state-of-public-rpc.mdx create mode 100644 src/lib/reports/loader.ts diff --git a/src/app/reports/[category]/[slug]/page.tsx b/src/app/reports/[category]/[slug]/page.tsx new file mode 100644 index 00000000..e6c21e83 --- /dev/null +++ b/src/app/reports/[category]/[slug]/page.tsx @@ -0,0 +1,432 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { MDXRemote } from "next-mdx-remote/rsc"; +import remarkGfm from "remark-gfm"; +import { StatTable } from "@/components/reports/stat-table"; +import { RegionWinners } from "@/components/reports/region-winners"; +import { + getAllReports, + getReport, + REPORT_CATEGORY_META, +} from "@/lib/reports/loader"; +import { safeJsonLd } from "@/lib/jsonld"; +import { PERSON_ID } from "@/lib/hub-jsonld"; +import { SITE } from "@/data/site"; + +export const revalidate = 3600; + +const SERIF: React.CSSProperties = { fontFamily: "var(--font-serif)" }; + +type Props = { params: Promise<{ category: string; slug: string }> }; + +export async function generateStaticParams() { + const reports = getAllReports(); + return reports.map((r) => ({ category: r.categorySlug, slug: r.slug })); +} + +export async function generateMetadata({ params }: Props): Promise { + const { category, slug } = await params; + const report = getReport(category, slug); + if (!report) return {}; + + const canonical = `${SITE.url}/reports/${category}/${slug}`; + const ogImage = report.ogImage ?? `${SITE.url}/opengraph-image`; + const social = `${report.title} · OpenChainBench`; + + return { + title: report.title, + description: report.summary, + alternates: { canonical }, + openGraph: { + title: social, + description: report.summary, + url: canonical, + type: "article", + siteName: "OpenChainBench", + publishedTime: new Date(report.publishedAt).toISOString(), + authors: [report.author], + images: [{ url: ogImage, width: 1200, height: 630 }], + }, + twitter: { + card: "summary_large_image", + title: social, + description: report.summary, + site: "@OpenChainBench", + images: [ogImage], + }, + }; +} + +function slugify(text: string): string { + return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); +} + +const MDX_COMPONENTS = { + StatTable, + RegionWinners, + + // Table of contents — pipe-separated section names, auto-generates anchor hrefs + TOC: ({ sections }: { sections: string }) => { + const items = sections.split("|").map((s) => s.trim()); + return ( + + ); + }, + + // Summary box — key findings list at article top + Summary: ({ children }: { children: React.ReactNode }) => ( + + ), + + // Orange highlighter on key sentences + Mark: ({ children }: { children: React.ReactNode }) => ( + + {children} + + ), + + // Decision Framework card — used explicitly in MDX + UseCase: ({ label, children }: { label: string; children: React.ReactNode }) => ( +
+

+ {label} +

+
+ {children} +
+
+ ), + + // Economist-style section header with scroll-target ID for TOC anchors + h2: (props: React.HTMLAttributes) => { + const text = typeof props.children === "string" ? props.children : ""; + return ( +
+

+ {props.children} +

+
+ ); + }, + + h3: (props: React.HTMLAttributes) => ( +

+ ), + + p: (props: React.HTMLAttributes) => ( +

+ ), + + ul: (props: React.HTMLAttributes) => ( +