From 05ce6bc2750db7f0979d3684b60a0be4294d16ba Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:50:33 -0500 Subject: [PATCH] fix(data): canonicalize locale pages --- packages/console/app/src/lib/stats-proxy.ts | 69 ++++++++++++++++++- .../stats/app/src/routes/[lab]/[model].tsx | 3 +- packages/stats/app/src/routes/[lab]/index.tsx | 3 +- packages/stats/app/src/routes/index.tsx | 3 +- packages/stats/app/src/routes/stats-cache.ts | 27 ++++++++ 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 packages/stats/app/src/routes/stats-cache.ts diff --git a/packages/console/app/src/lib/stats-proxy.ts b/packages/console/app/src/lib/stats-proxy.ts index e75ab33da5..94c50bcfc5 100644 --- a/packages/console/app/src/lib/stats-proxy.ts +++ b/packages/console/app/src/lib/stats-proxy.ts @@ -1,10 +1,15 @@ import type { APIEvent } from "@solidjs/start/server" import { Resource } from "@opencode-ai/console-resource" +import { LOCALE_HEADER, cookie, localeFromRequest, route, tag } from "~/lib/language" const dataPath = "/data" export async function statsProxy(evt: APIEvent) { const req = evt.request.clone() + const locale = localeFromRequest(req) + const redirect = redirectToLocalizedData(req, new URL(req.url), locale) + if (redirect) return redirect + const targetUrl = new URL(req.url) targetUrl.protocol = "https:" targetUrl.hostname = Resource.App.stage === "production" ? "stats.opencode.ai" : "stats.dev.opencode.ai" @@ -18,9 +23,13 @@ export async function statsProxy(evt: APIEvent) { targetUrl.pathname = targetUrl.pathname.slice(dataPath.length) } + const requestHeaders = new Headers(req.headers) + requestHeaders.set(LOCALE_HEADER, locale) + requestHeaders.set("accept-language", tag(locale)) + const response = await fetch(targetUrl, { method: req.method, - headers: req.headers, + headers: requestHeaders, body: req.body, }) @@ -30,6 +39,7 @@ export async function statsProxy(evt: APIEvent) { headers.delete("content-encoding") headers.delete("content-length") headers.delete("etag") + appendVary(headers, "Accept-Language", "Cookie") return new Response(rewriteStatsHtml(await response.text()), { status: response.status, @@ -52,3 +62,60 @@ export function statsRedirect(evt: APIEvent) { function rewriteStatsHtml(html: string) { return html.replaceAll('"/_build/', `"${dataPath}/_build/`).replaceAll("'/_build/", `'${dataPath}/_build/`) } + +function redirectToLocalizedData(request: Request, url: URL, locale: ReturnType) { + if (locale === "en") return null + if (request.headers.get(LOCALE_HEADER)) return null + if (request.method !== "GET" && request.method !== "HEAD") return null + if (!acceptsHtml(request)) return null + if (!url.pathname.startsWith(`${dataPath}/`) && url.pathname !== dataPath) return null + if (isDataBypassPath(url.pathname)) return null + + const next = new URL(url) + next.pathname = route(locale, url.pathname) + + const headers = new Headers({ + Location: next.toString(), + }) + headers.append("set-cookie", cookie(locale)) + appendVary(headers, "Accept-Language", "Cookie") + + return new Response(null, { + status: 308, + headers, + }) +} + +function acceptsHtml(request: Request) { + const accept = request.headers.get("accept") + return !accept || accept.includes("text/html") || accept.includes("*/*") +} + +function isDataBypassPath(pathname: string) { + return ( + pathname.startsWith(`${dataPath}/_build/`) || + pathname.startsWith(`${dataPath}/api/`) || + pathname.startsWith(`${dataPath}/_server`) || + pathname === `${dataPath}/banner.jpg` || + pathname === `${dataPath}/banner.png` + ) +} + +function appendVary(headers: Headers, ...values: string[]) { + const existing = headers + .get("vary") + ?.split(",") + .map((value) => value.trim()) + .filter(Boolean) + + headers.set( + "vary", + values + .reduce( + (result, value) => + result.some((item) => item.toLowerCase() === value.toLowerCase()) ? result : [...result, value], + existing ?? [], + ) + .join(", "), + ) +} diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx index 9bdcc49170..55287e8b72 100644 --- a/packages/stats/app/src/routes/[lab]/[model].tsx +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -31,6 +31,7 @@ import { type ModelCatalogEntry, } from "../model-catalog" import { runStatsEffect } from "../../stats-runtime" +import { setStatsPageCacheHeaders } from "../stats-cache" import { applyThemePreference, Footer, @@ -86,7 +87,7 @@ export default function StatsModel() { const i18n = useI18n() const language = useLanguage() const event = getRequestEvent() - event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400") + setStatsPageCacheHeaders(event?.response.headers) const params = useParams() const labParam = createMemo(() => params.lab ?? "") const modelParam = createMemo(() => params.model ?? "") diff --git a/packages/stats/app/src/routes/[lab]/index.tsx b/packages/stats/app/src/routes/[lab]/index.tsx index 61545e39b8..ab0583729a 100644 --- a/packages/stats/app/src/routes/[lab]/index.tsx +++ b/packages/stats/app/src/routes/[lab]/index.tsx @@ -21,6 +21,7 @@ import { type ModelCatalogLab, } from "../model-catalog" import { runStatsEffect } from "../../stats-runtime" +import { setStatsPageCacheHeaders } from "../stats-cache" import { applyThemePreference, Footer, @@ -43,7 +44,7 @@ export default function StatsLab() { const i18n = useI18n() const language = useLanguage() const event = getRequestEvent() - event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400") + setStatsPageCacheHeaders(event?.response.headers) const params = useParams() const labParam = createMemo(() => params.lab ?? "") const catalog = createAsync(() => getModelCatalog()) diff --git a/packages/stats/app/src/routes/index.tsx b/packages/stats/app/src/routes/index.tsx index eb23e5cb0c..87235e988e 100644 --- a/packages/stats/app/src/routes/index.tsx +++ b/packages/stats/app/src/routes/index.tsx @@ -32,6 +32,7 @@ import { useI18n } from "../context/i18n" import { useLanguage } from "../context/language" import { localizedUrl } from "../lib/language" import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog" +import { setStatsPageCacheHeaders } from "./stats-cache" import { applyThemePreference, Footer, @@ -121,7 +122,7 @@ export default function StatsHome() { const i18n = useI18n() const language = useLanguage() const event = getRequestEvent() - event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400") + setStatsPageCacheHeaders(event?.response.headers) const statsHomeUrl = localizedUrl(language.locale(), "/data/") const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString() const data = createAsync(() => getData()) diff --git a/packages/stats/app/src/routes/stats-cache.ts b/packages/stats/app/src/routes/stats-cache.ts new file mode 100644 index 0000000000..4ebf77914c --- /dev/null +++ b/packages/stats/app/src/routes/stats-cache.ts @@ -0,0 +1,27 @@ +const statsPageCacheControl = "public, max-age=60, s-maxage=300, stale-while-revalidate=86400" + +export function setStatsPageCacheHeaders(headers: Headers | undefined) { + if (!headers) return + + headers.set("Cache-Control", statsPageCacheControl) + appendVary(headers, "Accept-Language", "Cookie") +} + +function appendVary(headers: Headers, ...values: string[]) { + const existing = headers + .get("vary") + ?.split(",") + .map((value) => value.trim()) + .filter(Boolean) + + headers.set( + "vary", + values + .reduce( + (result, value) => + result.some((item) => item.toLowerCase() === value.toLowerCase()) ? result : [...result, value], + existing ?? [], + ) + .join(", "), + ) +}