Kaynağa Gözat

fix(data): canonicalize locale pages

Adam 1 ay önce
ebeveyn
işleme
05ce6bc275

+ 68 - 1
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<typeof localeFromRequest>) {
+  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(", "),
+  )
+}

+ 2 - 1
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 ?? "")

+ 2 - 1
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())

+ 2 - 1
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())

+ 27 - 0
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(", "),
+  )
+}