From 2f9cbcf4eb72d599ad5aadf44ad4e198caab91fb Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 5 Nov 2026 16:39:45 -0500 Subject: [PATCH] refactor(stats): serve opencode.ai/data straight from the stats worker (#62192) --- infra/stats.ts | 35 ++++++++++++-- packages/stats/README.md | 4 +- packages/stats/app/src/context/language.tsx | 2 + packages/stats/app/src/middleware.ts | 52 ++++++++++++++++++++- packages/stats/app/vite.config.ts | 20 ++++++++ sst.config.ts | 2 +- 6 files changed, 107 insertions(+), 8 deletions(-) diff --git a/infra/stats.ts b/infra/stats.ts index 58e3c6165aa3..146ec7104759 100644 --- a/infra/stats.ts +++ b/infra/stats.ts @@ -1,5 +1,6 @@ import { EMAILOCTOPUS_API_KEY } from "./app" -import { domain } from "./stage" +import { LOCALES } from "../packages/stats/app/src/lib/language" +import { domain, zoneID } from "./stage" //////////////// // DATABASE @@ -62,16 +63,42 @@ new sst.x.DevCommand("StatsStudio", { // APP //////////////// -export const app = new sst.cloudflare.x.SolidStart("Stats", { +export const publicUrl = `https://${domain}/data` + +const app = new sst.cloudflare.x.SolidStart("Stats", { path: "packages/stats/app", buildCommand: "bun run build", - domain: `stats.${domain}`, link: [database, EMAILOCTOPUS_API_KEY], environment: { - PUBLIC_URL: `https://${domain}/data`, + PUBLIC_URL: publicUrl, + }, + transform: { + server: { + url: false, + }, }, }) +// The site serves itself under /data and //data, and these routes are its only way in: it has no custom domain +// or workers.dev URL. Route patterns can't wildcard the middle of a path, hence one pair per locale. `sst dev` deploys +// no Worker. +const appServer = app.nodes.server +if (appServer) { + for (const prefix of ["data", ...LOCALES.map((locale) => `${locale}/data`)]) { + const name = prefix.replaceAll("/", "") + for (const [suffix, pattern] of [ + ["", `${domain}/${prefix}`], + ["Path", `${domain}/${prefix}/*`], + ]) { + new cloudflare.WorkersRoute(`StatsRoute${name}${suffix}`, { + zoneId: zoneID, + pattern, + script: appServer.nodes.worker.scriptName, + }) + } + } +} + //////////////// // SERVICES //////////////// diff --git a/packages/stats/README.md b/packages/stats/README.md index 7ed96ec4c287..3a5d6845efae 100644 --- a/packages/stats/README.md +++ b/packages/stats/README.md @@ -40,6 +40,6 @@ To run one sync pass on your machine against a stage's resources, run `bun sst s ## Public page caching -Stats HTML, JSON, and Markdown use a one-minute browser cache and a five-minute shared cache. Expired responses must revalidate instead of serving stale snapshots. The console `/data` proxy inherits this policy from the stats app. +Stats HTML, JSON, and Markdown use a one-minute browser cache and a five-minute shared cache. Expired responses must revalidate instead of serving stale snapshots. -Cloudflare must respect these origin headers. The live `Stats pages: respect short origin cache TTL` cache rule applies to `/data` and `/data/*` on `opencode.ai` and `stats.opencode.ai`: browser TTL respects origin, edge TTL uses cache-control headers and bypasses cache when absent, and serving stale content while updating is disabled. This dashboard-managed rule overrides the zone's four-hour browser-cache minimum; preserve it when changing Cloudflare configuration. Purge both stats URL prefixes after changing the policy to clear existing cached responses. +The Worker has no custom domain. `infra/stats.ts` routes `/data`, `/data/*`, `//data` and `//data/*` on the main host straight to it, so its responses skip the Cloudflare cache and reach browsers with these headers unchanged. Static assets are uploaded under `/data`, and the build moves Nitro's `_headers` to the assets root so they stay immutable. diff --git a/packages/stats/app/src/context/language.tsx b/packages/stats/app/src/context/language.tsx index 61c56bb6603c..0907eca321fd 100644 --- a/packages/stats/app/src/context/language.tsx +++ b/packages/stats/app/src/context/language.tsx @@ -49,6 +49,8 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont document.documentElement.lang = tag(store.locale) document.documentElement.dir = dir(store.locale) document.documentElement.dataset.locale = store.locale + // Remember a language picked by URL, so unprefixed /data links redirect back to it. + if (fromPathname(window.location.pathname)) document.cookie = cookie(store.locale) }) return { diff --git a/packages/stats/app/src/middleware.ts b/packages/stats/app/src/middleware.ts index 422be3228916..682da302584b 100644 --- a/packages/stats/app/src/middleware.ts +++ b/packages/stats/app/src/middleware.ts @@ -9,7 +9,18 @@ import { modelJson, modelMarkdown, } from "./lib/agent-formats" -import { fromPathname, LOCALE_HEADER, pageUrl, parseLocale, prefersMarkdown, route, strip } from "./lib/language" +import { + basePath, + cookie, + fromPathname, + LOCALE_HEADER, + localeFromRequest, + pageUrl, + parseLocale, + prefersMarkdown, + route, + strip, +} from "./lib/language" import { loadComparePage, loadHomePage, loadLabPage, loadModelPage } from "./lib/page-data" import { canonicalModelEntry, catalogModelPath, findModelCatalogLab, loadModelCatalog } from "./routes/model-catalog" import { statsPageCacheControl } from "./routes/stats-cache" @@ -41,6 +52,8 @@ export default createMiddleware({ const request = event.request if (request.method !== "GET" && request.method !== "HEAD") return const url = new URL(request.url) + const localized = localizedRedirect(request, url) + if (localized) return localized const path = strip(url.pathname) const parsed = parsePath(path) if (!parsed) return @@ -69,6 +82,43 @@ export default createMiddleware({ }, }) +// Unprefixed pages redirect to the visitor's language from their cookie or Accept-Language. /en/data stays put so +// English remains reachable for visitors whose browser prefers another language. +function localizedRedirect(request: Request, url: URL) { + if (fromPathname(url.pathname)) return undefined + if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return undefined + if (isDataBypassPath(url.pathname)) return undefined + const accept = request.headers.get("accept") + // Agents asking for Markdown get the English page in place. + if (prefersMarkdown(accept)) return undefined + if (accept && !accept.includes("text/html") && !accept.includes("*/*")) return undefined + const locale = localeFromRequest(request) + if (locale === "en") return undefined + const headers = new Headers({ + // route() normalizes /data to /data/, so keep the visitor's own trailing slash. + Location: `${url.pathname === basePath ? `/${locale}${basePath}` : route(locale, url.pathname)}${url.search}`, + "Cache-Control": "private, no-store", + Vary: "Accept, Accept-Language, Cookie", + }) + headers.append("Set-Cookie", cookie(locale)) + return new Response(null, { status: 308, headers }) +} + +function isDataBypassPath(pathname: string) { + return ( + pathname.startsWith(`${basePath}/_build/`) || + pathname.startsWith(`${basePath}/api/`) || + pathname.startsWith(`${basePath}/stats/`) || + pathname.startsWith(`${basePath}/_server`) || + pathname === `${basePath}/banner.jpg` || + pathname === `${basePath}/banner.png` || + pathname === `${basePath}/sitemap.xml` || + pathname.endsWith("/llms.txt") || + pathname.endsWith(".md") || + pathname.endsWith(".json") + ) +} + function parsePath(path: string) { const segments = path.split("/").filter(Boolean).slice(1) if (segments.some((segment) => !/^[\w.-]+$/.test(segment))) return undefined diff --git a/packages/stats/app/vite.config.ts b/packages/stats/app/vite.config.ts index 0bb578308d5e..068d0d91886b 100644 --- a/packages/stats/app/vite.config.ts +++ b/packages/stats/app/vite.config.ts @@ -1,3 +1,5 @@ +import { readFile, rm, writeFile } from "node:fs/promises" +import path from "node:path" import { solidStart } from "@solidjs/start/config" import { nitro } from "nitro/vite" import { defineConfig, type PluginOption } from "vite" @@ -7,12 +9,30 @@ export default defineConfig({ plugins: [ solidStart({ middleware: "./src/middleware.ts" }) as PluginOption, nitro({ + // The Worker owns opencode.ai/data/* directly, so static assets are uploaded under /data like the pages that + // reference them. Nitro's baseURL would do this too, but its router would then stop serving //data. + output: { publicDir: "{{ output.dir }}/public/data" }, compatibilityDate: "2024-09-19", preset: "cloudflare-module", cloudflare: { nodeCompat: true, }, }), + { + // Cloudflare only reads _headers at the assets root, but Nitro writes it into publicDir with unprefixed paths. + name: "stats:headers", + buildApp: { + order: "post", + async handler() { + const source = path.join(import.meta.dirname, ".output/public/data/_headers") + await writeFile( + path.join(import.meta.dirname, ".output/public/_headers"), + (await readFile(source, "utf8")).replace(/^\//gm, "/data/"), + ) + await rm(source) + }, + }, + }, ], server: { allowedHosts: true, diff --git a/sst.config.ts b/sst.config.ts index 19624722cb92..7a244c855fc6 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -43,7 +43,7 @@ export default $config({ await import("./infra/share.js") return { - ...(stats ? { StatsUrl: stats.app.url } : {}), + ...(stats ? { StatsUrl: stats.publicUrl } : {}), AwsStage: stage.awsStage, } },