mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 07:14:51 +00:00
refactor(stats): serve opencode.ai/data straight from the stats worker (#62192)
This commit is contained in:
parent
80bf7f7b0d
commit
2f9cbcf4eb
6 changed files with 107 additions and 8 deletions
|
|
@ -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 /<locale>/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
|
||||
////////////////
|
||||
|
|
|
|||
|
|
@ -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/*`, `/<locale>/data` and `/<locale>/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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 /<locale>/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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue