From 1dc2a8b1f5c0093575dacb6016a6b390184b6690 Mon Sep 17 00:00:00 2001 From: kevinschaich Date: Thu, 12 Nov 2026 04:21:27 -0700 Subject: [PATCH] Give each overview tab its own path instead of a # link Each tab is prerendered at /// with its own title and canonical URL so it can be indexed; old #tab links are moved to the path on load. Co-Authored-By: Claude Opus 5.5 --- src/app.tsx | 11 ++++++----- src/data.ts | 13 ++++++++++++- src/pages/overview.tsx | 38 +++++++++++++++++++------------------- vercel.json | 3 ++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index 5d9ba17..cb1c863 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,12 +1,13 @@ import { Byline } from '@kevinschaich/components' -import { BENCH, MODELS } from '@site/data' +import { BENCH, MODELS, TABS } from '@site/data' import { ModelPage } from '@site/pages/model' -import { Overview } from '@site/pages/overview' +import { Overview, tabFromPath } from '@site/pages/overview' -/** The page for a path under BENCH.path: the overview, or one model's page. */ +/** The page for a path under BENCH.path: an overview tab, or one model's page. */ export function App({ path }: { path: string }) { const id = path.replace(BENCH.path, '').replace(/\/$/, '') - const model = id ? MODELS.find(candidate => candidate.id === id) : undefined + const tab = tabFromPath(path) + const model = tab ? undefined : id ? MODELS.find(candidate => candidate.id === id) : undefined return (
@@ -18,7 +19,7 @@ export function App({ path }: { path: string }) {
- {model ? : } + {model ? : } ) } diff --git a/src/data.ts b/src/data.ts index dc1656b..03e9016 100644 --- a/src/data.ts +++ b/src/data.ts @@ -14,6 +14,14 @@ export const BENCH = { origin: 'https://darkgrade.com', } +/** The overview's tabs; each after the first is its own page at BENCH.path + id. */ +export const TABS = [ + { id: 'results', label: 'Results' }, + { id: 'tradeoff', label: 'Accuracy vs. speed' }, + { id: 'method', label: 'How we test' }, + { id: 'script', label: 'The script' }, +] + export interface Reason { area: string weight: number @@ -162,5 +170,8 @@ export function routes(): Route[] { }, }) ) - return [overview, ...models] + const tabs = TABS.slice(1).map( + (tab): Route => ({ ...overview, path: `${tab.id}/`, title: `${tab.label} | ${BENCH.name} by Darkgrade` }) + ) + return [overview, ...tabs, ...models] } diff --git a/src/pages/overview.tsx b/src/pages/overview.tsx index 2325417..4f93242 100644 --- a/src/pages/overview.tsx +++ b/src/pages/overview.tsx @@ -12,18 +12,11 @@ import { TabsTrigger, TradeoffChart, } from '@kevinschaich/components' -import { BENCH, FILTERS, MODELS, type ModelSummary, RUN, percent, sampleSentence, seconds } from '@site/data' +import { BENCH, FILTERS, TABS, MODELS, type ModelSummary, RUN, percent, sampleSentence, seconds } from '@site/data' import { Method } from '@site/pages/method' import { Script } from '@site/pages/script' import { useEffect, useState } from 'react' -const TABS = [ - { id: 'results', label: 'Results' }, - { id: 'tradeoff', label: 'Accuracy vs. speed' }, - { id: 'method', label: 'How we test' }, - { id: 'script', label: 'The script' }, -] - const COLUMNS: LeaderboardColumn[] = [ { key: 'model', @@ -52,12 +45,15 @@ const COLUMNS: LeaderboardColumn[] = [ }, ] -function tabFromHash(): string { - if (typeof location === 'undefined') return TABS[0].id - const id = location.hash.replace(/^#/, '') - return TABS.some(tab => tab.id === id) ? id : TABS[0].id +/** The tab a path under BENCH.path names; the first tab is the bench's root. */ +export function tabFromPath(path: string): string | undefined { + const id = path.replace(BENCH.path, '').replace(/\/$/, '') + if (!id) return TABS[0].id + return TABS.some(tab => tab.id === id) ? id : undefined } +const tabPath = (id: string): string => (id === TABS[0].id ? BENCH.path : `${BENCH.path}${id}/`) + /** One row of filters per category, each a one-of-a-few choice. */ function Filters({ chosen, choose }: { chosen: Record; choose: (filter: string, option: string) => void }) { return ( @@ -77,20 +73,24 @@ function Filters({ chosen, choose }: { chosen: Record; choose: ( ) } -export function Overview() { - const [tab, setTab] = useState(TABS[0].id) +export function Overview({ initialTab }: { initialTab: string }) { + const [tab, setTab] = useState(initialTab) const [chosen, setChosen] = useState>(Object.fromEntries(FILTERS.map(filter => [filter.id, 'all']))) const shown = MODELS.filter(model => FILTERS.every(filter => filter.options.find(option => option.id === chosen[filter.id])!.matches(model))) const choose = (filter: string, option: string) => setChosen(previous => ({ ...previous, [filter]: option })) useEffect(() => { - setTab(tabFromHash()) - const onHash = () => setTab(tabFromHash()) - window.addEventListener('hashchange', onHash) - return () => window.removeEventListener('hashchange', onHash) + const legacy = location.hash.replace(/^#/, '') + if (TABS.some(item => item.id === legacy)) { + history.replaceState(null, '', tabPath(legacy)) + setTab(legacy) + } + const onPop = () => setTab(tabFromPath(location.pathname) ?? TABS[0].id) + window.addEventListener('popstate', onPop) + return () => window.removeEventListener('popstate', onPop) }, []) const select = (id: string) => { setTab(id) - history.replaceState(null, '', `#${id}`) + history.pushState(null, '', tabPath(id)) } return ( diff --git a/vercel.json b/vercel.json index 85463ac..c3305d2 100644 --- a/vercel.json +++ b/vercel.json @@ -35,5 +35,6 @@ "destination": "/toolbench/", "permanent": false } - ] + ], + "trailingSlash": true }