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
13 changes: 11 additions & 2 deletions src/app/compare/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,19 @@ export async function generateMetadata({
params: Promise<Params>;
}): Promise<Metadata> {
const { slug } = await params;
// Run the same gating logic as the page render so non-canonical and
// invalid slugs short-circuit at the metadata phase, BEFORE Next.js
// streams the loading.tsx fallback. Without this the SSR HTML ends
// up with the root layout's homepage title and description plus the
// skeleton body, which is exactly what crawlers index. Routing here
// produces a real 308 / 404 response from the route layer instead of
// a 200 wrapping the streamed skeleton.
const canonicalTarget = canonicalisationTarget(slug);
if (canonicalTarget) redirect(`/compare/${canonicalTarget}`);
const pair = getComparePair(slug) ?? (await resolveAdHocPair(slug));
if (!pair) return {};
if (!pair) notFound();
const { a, b } = await loadPairProviders(pair);
if (!a || !b) return {};
if (!a || !b) notFound();

const title = `${a.name} vs ${b.name}: live OpenChainBench benchmark data`;
const description = capDescription(
Expand Down
51 changes: 46 additions & 5 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextResponse, type NextRequest } from "next/server";

/**
* Edge middleware. Three jobs:
* Edge middleware. Four jobs:
*
* 1. **Cache-key normalisation** on public read-only API routes that
* don't use query params. Vercel keys the edge cache on the full URL,
Expand All @@ -15,15 +15,26 @@ import { NextResponse, type NextRequest } from "next/server";
* on dev / staging only (bridge-revenue, evm-quote-latency, etc.).
* On prod they hit `notFound()` and return a 404. Sustained 404 on
* previously-indexed URLs gets read as a soft-404 signal that bleeds
* into surrounding bench rankingslikely the dominant driver of
* the recent brand-search collapse. Returning 410 explicitly tells
* Google "this URL is gone for good", and the cluster recovers.
* into surrounding bench rankings, likely the dominant driver of the
* recent brand-search collapse. Returning 410 explicitly tells Google
* "this URL is gone for good", and the cluster recovers.
*
* Only fires on production (`VERCEL_ENV === "production"`) so that
* staging / preview / local dev still render the bench pages from
* their YAML files normally.
*
* 3. **(future)** any cross-route concerns - kept lightweight.
* 3. **Canonical-order redirect on /compare/<a>-vs-<b>.** Pair pages
* have a single canonical URL: the alphabetical ordering of the two
* provider slugs. Without this, `/compare/base-vs-arbitrum` and
* `/compare/arbitrum-vs-base` both 200 with separate streamed
* Suspense fallbacks, which Google indexes as two empty shells of
* the same comparison. The page component also calls `redirect()`
* on non-canonical slugs but that fires after the loading.tsx
* boundary has streamed its skeleton, so the response goes out as
* 200 with the skeleton HTML and the homepage metadata. Doing it at
* the edge here short-circuits before any render starts.
*
* 4. **(future)** any cross-route concerns. Kept lightweight.
*/

const CANONICAL_NO_QUERY = new Set([
Expand All @@ -45,6 +56,12 @@ const REMOVED_BENCH_SLUGS = new Set([
]);

const BENCH_PATH = /^\/benchmarks\/([a-z0-9][a-z0-9-]{0,79})\/?$/;
// `/compare/<a>-vs-<b>` with both sides as standard provider slug
// shapes (lowercase alphanumeric + hyphens). The `-vs-` delimiter is
// matched literally; provider slugs themselves can contain hyphens
// (e.g. `helius-sender`, `phantom-perps`), so split on the first
// occurrence at parse time, not on every `-`.
const COMPARE_PATH = /^\/compare\/([a-z0-9][a-z0-9-]{0,79})\/?$/;

export function middleware(req: NextRequest) {
const { pathname, search } = req.nextUrl;
Expand All @@ -65,15 +82,39 @@ export function middleware(req: NextRequest) {
}
}

const compareMatch = pathname.match(COMPARE_PATH);
if (compareMatch) {
const canonical = canonicalComparePath(compareMatch[1]);
if (canonical && canonical !== compareMatch[1]) {
const url = req.nextUrl.clone();
url.pathname = `/compare/${canonical}`;
return NextResponse.redirect(url, 308);
}
}

return NextResponse.next();
}

/** Returns the alphabetical canonical form of a `<a>-vs-<b>` slug, or
* null when the slug isn't a valid pair shape. When the slug is already
* canonical the return value equals the input. */
function canonicalComparePath(slug: string): string | null {
const idx = slug.indexOf("-vs-");
if (idx <= 0) return null;
const a = slug.slice(0, idx);
const b = slug.slice(idx + "-vs-".length);
if (!a || !b || a === b) return null;
const [first, second] = [a, b].sort();
return `${first}-vs-${second}`;
}

export const config = {
matcher: [
"/api/citable",
"/api/llm-context",
"/api/freshness",
"/api/openapi.json",
"/benchmarks/:slug*",
"/compare/:slug*",
],
};
Loading