From 3d08c4449b9653193b6ecd44acacde331908ea8d Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:52:11 +0200 Subject: [PATCH] perf(seo): cache OG renders, SSR home ticker snapshot, fee-compare JSON-LD --- src/app/api/og/[slug]/route.tsx | 13 +++++- src/app/benchmarks/[slug]/opengraph-image.tsx | 2 +- src/app/benchmarks/[slug]/twitter-image.tsx | 2 +- src/app/fee-compare/page.tsx | 32 +++++++++++++++ src/app/page.tsx | 41 ++++++++++++++++++- .../[category]/[slug]/opengraph-image.tsx | 2 +- src/components/live/dashboard.tsx | 12 +++++- 7 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/app/api/og/[slug]/route.tsx b/src/app/api/og/[slug]/route.tsx index d2449071..3a8cae4f 100644 --- a/src/app/api/og/[slug]/route.tsx +++ b/src/app/api/og/[slug]/route.tsx @@ -129,7 +129,18 @@ export async function GET( ), - size, + { + ...size, + // Every social/AI unfurl used to trigger a full satori render + // (max-age=0 → x-vercel-cache MISS on consecutive GETs). The + // underlying data moves every 60s but a 1h-old card is fine for + // an unfurl; s-maxage bounds renders to ~24/day/slug while SWR + // keeps scrapes instant. + headers: { + "cache-control": + "public, s-maxage=3600, stale-while-revalidate=86400", + }, + }, ); } diff --git a/src/app/benchmarks/[slug]/opengraph-image.tsx b/src/app/benchmarks/[slug]/opengraph-image.tsx index 4b50596e..c46b72f7 100644 --- a/src/app/benchmarks/[slug]/opengraph-image.tsx +++ b/src/app/benchmarks/[slug]/opengraph-image.tsx @@ -153,6 +153,6 @@ export default async function OG({ ), - { ...size } + { ...size, headers: { "cache-control": "public, s-maxage=3600, stale-while-revalidate=86400" } } ); } diff --git a/src/app/benchmarks/[slug]/twitter-image.tsx b/src/app/benchmarks/[slug]/twitter-image.tsx index f720ed0e..88fec66d 100644 --- a/src/app/benchmarks/[slug]/twitter-image.tsx +++ b/src/app/benchmarks/[slug]/twitter-image.tsx @@ -162,6 +162,6 @@ export default async function TwitterImage({ ), - { ...size } + { ...size, headers: { "cache-control": "public, s-maxage=3600, stale-while-revalidate=86400" } } ); } diff --git a/src/app/fee-compare/page.tsx b/src/app/fee-compare/page.tsx index 3cc0f946..7baf3288 100644 --- a/src/app/fee-compare/page.tsx +++ b/src/app/fee-compare/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { pageMetadata } from "@/lib/page-metadata"; import { FeeCompareClient } from "@/components/fee-compare-client"; +import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; export const metadata: Metadata = pageMetadata({ path: "/fee-compare", @@ -26,8 +28,38 @@ export default async function FeeComparePage({ const rawDays = parseInt(params.days ?? "90", 10); const initialDays = isFinite(rawDays) ? Math.min(180, Math.max(7, rawDays)) : 90; + // Only page on the site without JSON-LD until 2026-09: emit the same + // BreadcrumbList shape every hub page ships, plus a WebApplication + // node describing the comparison tool itself. + const jsonLd = { + "@context": "https://schema.org", + "@graph": [ + buildBreadcrumbJsonLd([ + { name: "Home", item: SITE.url }, + { name: "Perp DEX fee comparison", item: `${SITE.url}/fee-compare` }, + ]), + { + "@type": "WebApplication", + "@id": `${SITE.url}/fee-compare#app`, + name: "Perp DEX fee comparison", + url: `${SITE.url}/fee-compare`, + applicationCategory: "FinanceApplication", + operatingSystem: "Web", + offers: { "@type": "Offer", price: "0", priceCurrency: "USD" }, + description: + "Compare taker fees between any two perp DEXs. Paste a wallet to see what was actually paid on Hyperliquid or Gains and what the same trades would have cost elsewhere.", + publisher: { "@id": `${SITE.url}/#org` }, + }, + ], + }; + return (
+