Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,31 @@ const nextConfig: NextConfig = {
},
],
},
{
// Sitemap: force-dynamic in the route file (Data Cache 2MB cap
// blows past for the 500+ URL corpus), so nothing caches it by
// default and every crawler hit re-runs the full loader chain.
// Hold it edge-side for an hour with SWR so cold hits stay fast.
source: "/sitemap.xml",
headers: [
{
key: "Cache-Control",
value: "public, s-maxage=3600, stale-while-revalidate=86400",
},
],
},
{
// Citable JSON is polled by LLM crawlers (Perplexity, ChatGPT,
// Claude Deep Research) — a bare `public` with no s-maxage sent
// every scrape to origin. Same freshness window as sitemap.
source: "/api/citable",
headers: [
{
key: "Cache-Control",
value: "public, s-maxage=3600, stale-while-revalidate=86400",
},
],
},
];
},
async redirects() {
Expand Down
43 changes: 11 additions & 32 deletions src/app/benchmarks/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,17 @@ export async function generateStaticParams() {
return [];
}

// Emit one OG image per (slug, chain) combo so social shares of
// `/benchmarks/{slug}?chain=X` render a chain-honest card instead of the
// cross-chain aggregate leader (which is the misleading baseline-skew
// case bench-001 ran into: GMGN looking like "fastest crypto data API"
// on the unfiltered view because Solana's faster baseline drags the
// average down). The `id` Next.js threads through to the default image
// handler is the chain value; the unfiltered card uses `id="default"`.
export async function generateImageMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// Load editorial-only via getBenchmark (memoised; safe at build time).
const b = await getBenchmark(slug);
const chains = (b?.dimensions?.chain ?? []).filter(
(c) => c.value !== "all",
);
return [
{
id: "default",
alt,
size,
contentType,
},
...chains.map((c) => ({
id: c.value,
alt: `${alt}. ${c.label}`,
size,
contentType,
})),
];
// Emit a SINGLE OG image for the aggregate bench page. Previously we
// fanned out one entry per chain variant so `/benchmarks/{slug}?chain=X`
// social shares could render a chain-honest card, but Next hoists every
// generateImageMetadata entry into a separate <meta property="og:image">
// on the parent page — X, LinkedIn, Slack pick nondeterministically
// among them, so a share of `?chain=solana` might render the `robinhood`
// card. Chain-scoped pages live at `/benchmarks/[slug]/[chain]` and can
// carry their own opengraph-image handler if per-chain cards become a
// priority; today they inherit the site-wide default OG.
export async function generateImageMetadata() {
return [{ id: "default", alt, size, contentType }];
}

export default async function OG({
Expand Down
12 changes: 11 additions & 1 deletion src/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { ArrowLeft } from "lucide-react";

export const metadata: Metadata = {
title: "Page not found",
robots: { index: false },
// Full robots shape so Next replaces (not merges) the layout's robots
// meta. Previous shape `{ index: false }` caused two `<meta name="robots"
// content="noindex">` tags on prod 404s (layout emitted one from its
// own defaults, this route emitted a second) — Bing had been observed
// to ignore the duplicated tag and fall through to the layout's
// index=true. Match the shape used by the layout's IS_STAGING branch.
robots: {
index: false,
follow: false,
googleBot: { index: false, follow: false },
},
};

export default function NotFound() {
Expand Down
16 changes: 16 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ const COMPARE_PATH = /^\/compare\/([a-z0-9][a-z0-9-]{0,79})\/?$/;
export function middleware(req: NextRequest) {
const { pathname, search } = req.nextUrl;

// Case normalisation. `/products/Alchemy` and `/Benchmarks/foo` used
// to serve 200 with the mixed-case URL while the HTML canonical
// pointed at the lowercase form — Google indexed the mixed-case URL
// as a valid variant and burned crawl budget on both. Force lowercase
// via 308 so only the canonical shape reaches origin.
if (pathname !== pathname.toLowerCase()) {
const url = req.nextUrl.clone();
url.pathname = pathname.toLowerCase();
return NextResponse.redirect(url, 308);
}

if (search && CANONICAL_NO_QUERY.has(pathname)) {
const canonical = req.nextUrl.clone();
canonical.search = "";
Expand Down Expand Up @@ -136,6 +147,11 @@ export const config = {
"/api/llm-context",
"/api/freshness",
"/api/openapi.json",
// Wildcard `/((?!_next|.*\..*).*)` covers every HTML route so the
// case-normalisation redirect fires on any URL, not just the
// enumerated bench/answer/compare/product paths. Excludes _next
// internals and any file with an extension (assets, RSS, sitemap).
"/((?!_next|api|.*\\..*).*)",
"/benchmarks/:slug*",
"/answers/:slug*",
"/compare/:slug*",
Expand Down
Loading