From b594ff6f98523e0b1593d692ec11ca2ce357d6e9 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:49:28 +0200 Subject: [PATCH 1/5] Update architecture.mdx --- clickstack/architecture.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clickstack/architecture.mdx b/clickstack/architecture.mdx index b0eaa07b7..ae002f5c7 100644 --- a/clickstack/architecture.mdx +++ b/clickstack/architecture.mdx @@ -9,6 +9,8 @@ keywords: ['ClickStack architecture', 'observability architecture', 'HyperDX', ' import { Image } from "/snippets/components/Image.jsx"; +Test change + The ClickStack architecture varies depending on how it is deployed. There are important architectural distinctions between **ClickStack Open Source**, where all components are self-managed, and **Managed ClickStack**, where ClickHouse and the HyperDX UI are hosted and operated in ClickHouse Cloud. While the core components remain the same in both models, the responsibility for hosting, scaling, and securing each component differs. ## Architecture overview {#architecture-overview} From 396690df47dcc55500a3f664fa0d9fcd8161fa10 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:09:25 +0200 Subject: [PATCH 2/5] Optimize localized documentation builds --- .gitignore | 1 + astro.config.ts | 18 +- bin/vercel-build.ts | 87 ++++- package.json | 2 +- pnpm-lock.yaml | 303 ++++++------------ src/README.md | 24 +- src/components/api/ApiPage.astro | 2 +- src/content.config.ts | 14 +- src/lib/corpus.ts | 3 +- src/lib/locale-content.server.ts | 21 ++ src/lib/openapi.ts | 15 +- src/lib/scope.ts | 30 +- src/lib/sidebar-lazy.ts | 129 +++++--- src/lib/sidebar-navigation.server.ts | 30 ++ src/pages/[...corpus]/llms.txt.ts | 4 + src/pages/[...slug].astro | 11 +- src/pages/[...slug].md.ts | 10 +- src/pages/[...slug].mdx.ts | 30 -- src/pages/[...slug]/index.md.ts | 28 -- src/pages/[...slug]/index.mdx.ts | 25 -- src/pages/[locale]/[...slug].astro | 61 ++-- src/pages/[locale]/[...slug]/index.md.ts | 54 ---- src/pages/[locale]/[section]/llms-full.txt.ts | 23 -- src/pages/[locale]/index.astro | 27 +- src/pages/[locale]/llms-full.txt.ts | 18 -- src/pages/[locale]/llms.txt.ts | 37 --- src/pages/[section]/llms-full.txt.ts | 7 +- src/pages/changelog/[product]/[...slug].astro | 9 +- src/pages/changelog/[product]/index.astro | 12 +- src/pages/changelog/[product]/rss.xml.ts | 12 +- .../api-reference/[tag]/[operation].astro | 6 +- .../index.md.ts => [operation].md.ts} | 10 +- .../[tag]/[operation]/index.mdx.ts | 15 - src/pages/index.astro | 3 +- src/pages/nav/[...key].astro | 21 +- .../api-reference/[tag]/[operation].astro | 6 +- .../index.md.ts => [operation].md.ts} | 10 +- .../[tag]/[operation]/index.mdx.ts | 15 - tsconfig.json | 3 +- vercel.json | 35 ++ 40 files changed, 570 insertions(+), 601 deletions(-) create mode 100644 src/lib/locale-content.server.ts create mode 100644 src/lib/sidebar-navigation.server.ts delete mode 100644 src/pages/[...slug].mdx.ts delete mode 100644 src/pages/[...slug]/index.md.ts delete mode 100644 src/pages/[...slug]/index.mdx.ts delete mode 100644 src/pages/[locale]/[...slug]/index.md.ts delete mode 100644 src/pages/[locale]/[section]/llms-full.txt.ts delete mode 100644 src/pages/[locale]/llms-full.txt.ts delete mode 100644 src/pages/[locale]/llms.txt.ts rename src/pages/clickstack/api-reference/[tag]/{[operation]/index.md.ts => [operation].md.ts} (60%) delete mode 100644 src/pages/clickstack/api-reference/[tag]/[operation]/index.mdx.ts rename src/pages/products/cloud/api-reference/[tag]/{[operation]/index.md.ts => [operation].md.ts} (60%) delete mode 100644 src/pages/products/cloud/api-reference/[tag]/[operation]/index.mdx.ts diff --git a/.gitignore b/.gitignore index 989e05f7f..7e7ef9d45 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ dist/ .astro/ .remote/ +.vercel-build/ .temp/ tmp/ src/generated/ diff --git a/astro.config.ts b/astro.config.ts index 73424e50f..9655d3d24 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -25,9 +25,8 @@ import { clickhouseSqlTransformer } from "./src/plugins/shiki-clickhouse-sql"; // The site is served at clickhouse.com/docs behind the website Worker. export const BASE = "/docs"; const buildScope = readScope(); -// Locale Workers share public images and Nimbus's static CSS through the -// English Worker, but their compiled chunks must never collide. The website -// router sends /docs/_astro-/* to the matching locale Worker. +// Locale build shards share public images from the English output, but their +// compiled chunks must never collide when all shards are merged. const buildAssetsDirectory = buildScope.locale === "en" ? "_astro" : `_astro-${buildScope.locale.toLowerCase()}`; @@ -39,6 +38,10 @@ const remoteMounts = (JSON.parse(fs.readFileSync(new URL("./remotes.json", impor // pnpm install does not expose those transitive links to Rolldown, so use the // vendored ESM runtime instead of relying on node_modules symlink layout. const tslibModule = fileURLToPath(new URL("./src/shims/tslib.mjs", import.meta.url)); +const activeHomepageModule = fileURLToPath(new URL( + `./src/generated/homepage/${buildScope.locale.toLowerCase()}.jsx`, + import.meta.url, +)); function markdownSnippet(relativePath: string) { const source = fs.readFileSync(new URL(relativePath, import.meta.url), "utf8") @@ -73,13 +76,16 @@ export default defineConfig({ base: BASE, output: "static", build: { assets: buildAssetsDirectory }, - publicDir: "./.remote/public-build", - // Overridable so parallel builds (CI shards, concurrent sessions) never + publicDir: process.env.DOCS_SKIP_PUBLIC === "1" ? "./.remote/public-empty" : "./.remote/public-build", + // Overridable so sequential locale shards and concurrent sessions never // share an output directory or the content-layer cache. outDir: process.env.DOCS_OUT_DIR ?? "./dist", cacheDir: process.env.DOCS_CACHE_DIR ?? "./node_modules/.astro", trailingSlash: "ignore", prefetch: { prefetchAll: true, defaultStrategy: "hover" }, + experimental: { + incrementalBuild: true, + }, markdown: { // Mermaid fences are rendered client-side (src/plugins/satteri-mermaid.ts). syntaxHighlight: { type: "shiki", excludeLangs: ["mermaid"] }, @@ -140,7 +146,7 @@ export default defineConfig({ }, plugins: [tailwindcss(), mintlifySnippets()], resolve: { - alias: { tslib: tslibModule }, + alias: { tslib: tslibModule, "@active-homepage": activeHomepageModule }, dedupe: ["react", "react-dom"], }, }, diff --git a/bin/vercel-build.ts b/bin/vercel-build.ts index bb0144b7a..4854d53dd 100644 --- a/bin/vercel-build.ts +++ b/bin/vercel-build.ts @@ -7,6 +7,10 @@ * post-processing run after every credential has been removed. */ import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { readScope, type Locale } from "../src/lib/scope.ts"; +import { localeRouteName } from "../src/util/locales.ts"; const root = process.cwd(); const credentialVariables = [ @@ -44,6 +48,35 @@ function run(command: string, args: string[], environment: NodeJS.ProcessEnv): v execFileSync(command, args, { cwd: root, env: environment, stdio: "inherit" }); } +function shardEnvironment( + environment: NodeJS.ProcessEnv, + locale: "en" | Locale, + availableLocales: Locale[], + outDir: string, +): NodeJS.ProcessEnv { + const child = { ...environment }; + delete child.DOCS_LOCALES; + child.DOCS_BUILD_SHARD = "1"; + child.DOCS_LOCALE = locale; + child.DOCS_AVAILABLE_LOCALES = availableLocales.join(",") || "none"; + child.DOCS_EMIT_ENGLISH = locale === "en" ? "true" : "false"; + child.DOCS_OUT_DIR = outDir; + child.DOCS_CACHE_DIR = path.join(root, "node_modules", ".astro", locale.toLowerCase()); + if (locale === "en") delete child.DOCS_SKIP_PUBLIC; + else child.DOCS_SKIP_PUBLIC = "1"; + return child; +} + +function copyDirectory(source: string, destination: string, merge = false): void { + if (!fs.existsSync(source)) { + throw new Error(`vercel-build: expected shard output ${path.relative(root, source)} does not exist`); + } + if (!merge && fs.existsSync(destination)) { + throw new Error(`vercel-build: refusing to overwrite merged output ${path.relative(root, destination)}`); + } + fs.cpSync(source, destination, { recursive: true, force: merge, errorOnExist: !merge }); +} + // The fetch child is the only process allowed to see the deployment OIDC // identity. It downloads bytes but never parses or imports remote-authored MDX. run(process.execPath, ["bin/fetch-remotes.ts"], { ...process.env }); @@ -54,5 +87,55 @@ const cleanEnvironment = sanitizedEnvironment(); assertCredentialFree(cleanEnvironment); run("pnpm", ["run", "prepare:site"], cleanEnvironment); -run("pnpm", ["exec", "astro", "build"], cleanEnvironment); -run(process.execPath, ["bin/postbuild.ts"], cleanEnvironment); + +const scope = readScope(root); +const finalOutDir = path.resolve(root, cleanEnvironment.DOCS_OUT_DIR ?? "dist"); +if (scope.locales.length === 0) { + const environment = shardEnvironment(cleanEnvironment, "en", [], finalOutDir); + run("pnpm", ["exec", "astro", "build"], environment); + run(process.execPath, ["bin/postbuild.ts"], environment); + process.exit(0); +} + +// Each locale gets its own process, module graph, content collection, output, +// and persistent Astro cache. Peak memory is bounded by one locale build while +// the final deployment remains a single Vercel artifact. +const shardsRoot = path.join(root, ".vercel-build", "shards"); +fs.rmSync(shardsRoot, { recursive: true, force: true }); +fs.mkdirSync(shardsRoot, { recursive: true }); +fs.mkdirSync(path.join(root, ".remote", "public-empty"), { recursive: true }); + +const englishOutDir = path.join(shardsRoot, "en"); +run( + "pnpm", + ["exec", "astro", "build"], + shardEnvironment(cleanEnvironment, "en", scope.locales, englishOutDir), +); + +const localeOutputs: Array<{ locale: Locale; outDir: string }> = []; +for (const locale of scope.locales) { + const outDir = path.join(shardsRoot, locale.toLowerCase()); + run( + "pnpm", + ["exec", "astro", "build"], + shardEnvironment(cleanEnvironment, locale, scope.locales, outDir), + ); + localeOutputs.push({ locale, outDir }); +} + +fs.rmSync(finalOutDir, { recursive: true, force: true }); +copyDirectory(englishOutDir, finalOutDir); +for (const { locale, outDir } of localeOutputs) { + copyDirectory( + path.join(outDir, localeRouteName(locale)), + path.join(finalOutDir, localeRouteName(locale)), + true, + ); + copyDirectory( + path.join(outDir, `_astro-${locale.toLowerCase()}`), + path.join(finalOutDir, `_astro-${locale.toLowerCase()}`), + ); +} + +const postbuildEnvironment = { ...cleanEnvironment, DOCS_OUT_DIR: finalOutDir }; +run(process.execPath, ["bin/postbuild.ts"], postbuildEnvironment); diff --git a/package.json b/package.json index add93a8b9..7acc0948d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@readme/httpsnippet": "11.4.0", "@scalar/openapi-parser": "0.28.12", "@vercel/connect": "^2.0.2", - "astro": "~7.0.9", + "astro": "~7.3.2", "clsx": "^2.1.1", "mermaid": "^11.17.2", "openapi-sampler": "1.7.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da3086025..b84dacb24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: version: 5.0.7(@types/node@24.13.3)(@types/react-dom@19.2.6(@types/react@19.2.18))(@types/react@19.2.18)(jiti@2.7.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(yaml@2.9.0) '@cloudflare/nimbus-docs': specifier: 0.13.1 - version: 0.13.1(patch_hash=291b76e22c5f035fd612c4f8086e9026262c2ecbe5af3de58a9603413f4838b0)(@readme/httpsnippet@11.4.0)(@scalar/openapi-parser@0.28.12)(@types/node@24.13.3)(astro@7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0))(esbuild@0.28.2)(jiti@2.7.0)(openapi-sampler@1.7.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 0.13.1(patch_hash=291b76e22c5f035fd612c4f8086e9026262c2ecbe5af3de58a9603413f4838b0)(@readme/httpsnippet@11.4.0)(@scalar/openapi-parser@0.28.12)(@types/node@24.13.3)(astro@7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))(esbuild@0.28.2)(jiti@2.7.0)(openapi-sampler@1.7.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@fontsource-variable/inter': specifier: ^5.2.8 version: 5.3.0 @@ -50,8 +50,8 @@ importers: specifier: ^2.0.2 version: 2.0.2 astro: - specifier: ~7.0.9 - version: 7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0) + specifier: ~7.3.2 + version: 7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -128,65 +128,65 @@ packages: peerDependencies: typescript: ^5.0.0 || ^6.0.0 - '@astrojs/compiler-binding-darwin-arm64@0.3.2': - resolution: {integrity: sha512-MM8tn8CSimcfytaOla4b6acN8mKWiL/rlAA1fpT3/Wl7dNGSE4y8FjTN/zJVNnb63CsLWG5zZwCt01TXtDKh9g==} + '@astrojs/compiler-binding-darwin-arm64@0.4.0': + resolution: {integrity: sha512-ZVUwHundaQyFNjE6uoa0usaC0WOCitDCLS/4mdb4rOiJXwVUuKJBMxI5WMzXLWmamsXtK/Z//ifLXvV5Yeh4Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.3.2': - resolution: {integrity: sha512-2lXOlzf8xb7jLomRsf/aswh61/NnGusynB2OwFkK6k4pmOtpfXMYnG0PLfXrEvxXYj69NdCnmUYXtHDd+JOOag==} + '@astrojs/compiler-binding-darwin-x64@0.4.0': + resolution: {integrity: sha512-FI6G8AY8u6fR1SI/QRR5yGMwtvZwP34CDmZpZ5HwJGa50UM1VISTLhqkhV4a476pmgd25X1Aur2dqw6hUnrlKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': - resolution: {integrity: sha512-BmU3kWj7qnLrd4vzm49zFEPJ5oFnn1tCT4Vt9hZbqdU5Cmb8GZl7fn6VFsnNfe7B18a2gIFtVzbLINtYl5kBjQ==} + '@astrojs/compiler-binding-linux-arm64-gnu@0.4.0': + resolution: {integrity: sha512-lB9gLFJK7m82EnjaU8nlRBEfcwGNeHidW3sSjODTUjMNaoewVuUz9fwwdY5M4jiSXIqWLH3yl6TX8FTDKA74Sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': - resolution: {integrity: sha512-f0heT9ZZEseSu5bHCeb80eL2DH07ArE6U9xi1WT/PEusNjzPmEr3GJsjG1tRLo5VYUUYX7h3ScaqGmGrMOVGmw==} + '@astrojs/compiler-binding-linux-arm64-musl@0.4.0': + resolution: {integrity: sha512-HPbvWqbxFxyaoQJhLxCaSjtYBx9KBo7JGVzEFZCmMl968a2PsSH0UfiODYgYPXofTOIsIH2aoCcrHXML0IA3ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': - resolution: {integrity: sha512-M8fOUt0itRpqiGyoEA/ij184s8O+hqbCz3+YozRusOOM3osgGljpDThhbKAJjqh82wOo6FioQ4w8PBvU1XMD5Q==} + '@astrojs/compiler-binding-linux-x64-gnu@0.4.0': + resolution: {integrity: sha512-tQKolMxoJ/+0AmLWm1PmJ/i+z3i10ZU1bNuVjEDulCf48azEMtUNjTZgHJ5MPtpYRNc7dlETr8QujUfduzoC7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@astrojs/compiler-binding-linux-x64-musl@0.3.2': - resolution: {integrity: sha512-/Kebk8sO6HnLeSd691JkaAPfN7CqR9/KEXmWvyNPkaKNGmj8rTZ/lf2uXtnPu92Lan84UrKdIKVPy1fSo2encQ==} + '@astrojs/compiler-binding-linux-x64-musl@0.4.0': + resolution: {integrity: sha512-5v5YymudsxMHp3NBLCS8BUlu5CRqeLtWD9cKS/4nIhIEHCbpz9okmVV6I0HWqmBAPhWYcDa3vw/vltYPrOQCTA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@astrojs/compiler-binding-wasm32-wasi@0.3.2': - resolution: {integrity: sha512-pUA6xbcOSB7DhfzIArB8BCAkFfAIqriiR7zl5zOStd6oU2G0kIKj+GUdGnyXyhfiv881Hffyk5tC0mR18sDjDw==} + '@astrojs/compiler-binding-wasm32-wasi@0.4.0': + resolution: {integrity: sha512-m/phuH3x3PREvv1OnkM44NoPh4MatUadix1fB1u5SvMLCyDTUZykDJbKnWf1cjnYmHdlB8HcjTjl6JrCqAIXcw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': - resolution: {integrity: sha512-ESruf+6Qkl1trHUFxI6GSf6t52j8yN2kCNSzMWdzt7V/T09tFHrYzrVaJQohb2C9bJUH76pNvX6Zb51+xCQc9Q==} + '@astrojs/compiler-binding-win32-arm64-msvc@0.4.0': + resolution: {integrity: sha512-B9zYf3okEY83kM8gydlpH2BHP00w4ifxPqlYlWrgTwuD6wnkrJDCwBlgy1q31cERjCJRXN1lrE2VmkLvFjv/6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': - resolution: {integrity: sha512-wzzVrEbOwbsLWOdEbocskjMRx2aZPxJ7ZbmL+jnpBamFwmigm+2M/wzuM6JWncocgYwLic1csSpalBh96kQKXA==} + '@astrojs/compiler-binding-win32-x64-msvc@0.4.0': + resolution: {integrity: sha512-zB0Nrv0dGc0zZWPGDRmmETTPhDRqyZjAjk+gWMlVrJX5U89obpB3VUUE1ZiHxOCN5LQojeLK6O8L/dnoHolvNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@astrojs/compiler-binding@0.3.2': - resolution: {integrity: sha512-8w/9CWmYrAJJ8N0SY3O43ws2BgxoW6u3QsD8u2mE140lMYAlwh+tlNoUeSBq22wVheFuiBbR212l6ixZ2IIgCQ==} + '@astrojs/compiler-binding@0.4.0': + resolution: {integrity: sha512-x2RjDUuWfwLNtc3mjAdSRInwqh/rqbLar9cm/5FOMbHvmYZB7yfKewzSclAxWjIZsypJDXv1lhaP2WG+P8TK3g==} engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-rs@0.3.2': - resolution: {integrity: sha512-xlx/T7JovIKduu4ucbTQUxQ5+Q8wxkHxhLjnZk3VlJbhbQ9RLvvuDk1p2YYFYFQ5y14dVm3FGGO4isQXa4F+Tg==} + '@astrojs/compiler-rs@0.4.0': + resolution: {integrity: sha512-koVikeon1kreEy+/JzLQRy3vzHHQVOjycs4degg4vFufKApZOwMZvSSAEztYNhmcQVfNVsVZZI4cEge3cexAbQ==} engines: {node: '>=22.12.0'} '@astrojs/compiler@2.13.1': @@ -195,12 +195,12 @@ packages: '@astrojs/internal-helpers@0.10.0': resolution: {integrity: sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==} - '@astrojs/internal-helpers@0.10.1': - resolution: {integrity: sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q==} - '@astrojs/internal-helpers@0.10.4': resolution: {integrity: sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA==} + '@astrojs/internal-helpers@0.11.0': + resolution: {integrity: sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==} + '@astrojs/language-server@2.16.16': resolution: {integrity: sha512-KuS17AOOqH/M5mHXPmKvtp8L+NNteARC7Xjf395+9R9dtJOBndqdStwU4V+OKzYZpgZ+hliV13knzx/oMtFl7Q==} hasBin: true @@ -216,12 +216,12 @@ packages: '@astrojs/markdown-remark@7.2.4': resolution: {integrity: sha512-MvspGMynWKAjTe4/lTUdmBPHIFKNVLTCF6UlyWGogTGzNrTvjD+D4n48k7h8swxsEPKHK2TwxkZO7uoaCv1Pow==} - '@astrojs/markdown-satteri@0.3.4': - resolution: {integrity: sha512-6Lvt/bQZEBW+zzdhPblvfZEy5PGEYJaUsUqaCgwHeRPxZJL1gc9I+DRLKWJjjYTWDzVUTzXlMq4WwSK+X34CVw==} - '@astrojs/markdown-satteri@0.3.8': resolution: {integrity: sha512-n8ItpFTCmlDsVR5+rwDmehSf+jFCYLWmZiisZNGuF7xILqYhsVeBfcha4qgS2Seq3fAc9Tm58ZVrvqFQ6RQgRQ==} + '@astrojs/markdown-satteri@0.4.1': + resolution: {integrity: sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg==} + '@astrojs/mdx@7.0.8': resolution: {integrity: sha512-RNuwq2ccTSi7NX9YqlR0noaWoVdOrZuJvdfWXotAzdhMVB0b0zEMLnNU+xar7le0GwTMlTObD/QAu8jeHA/3nA==} engines: {node: '>=22.12.0'} @@ -350,91 +350,46 @@ packages: cpu: [arm64] os: [darwin] - '@bruits/satteri-darwin-arm64@0.9.5': - resolution: {integrity: sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow==} - cpu: [arm64] - os: [darwin] - '@bruits/satteri-darwin-x64@0.10.5': resolution: {integrity: sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==} cpu: [x64] os: [darwin] - '@bruits/satteri-darwin-x64@0.9.5': - resolution: {integrity: sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q==} - cpu: [x64] - os: [darwin] - '@bruits/satteri-linux-arm64-gnu@0.10.5': resolution: {integrity: sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==} cpu: [arm64] os: [linux] - '@bruits/satteri-linux-arm64-gnu@0.9.5': - resolution: {integrity: sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA==} - cpu: [arm64] - os: [linux] - '@bruits/satteri-linux-arm64-musl@0.10.5': resolution: {integrity: sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==} cpu: [arm64] os: [linux] - '@bruits/satteri-linux-arm64-musl@0.9.5': - resolution: {integrity: sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ==} - cpu: [arm64] - os: [linux] - '@bruits/satteri-linux-x64-gnu@0.10.5': resolution: {integrity: sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==} cpu: [x64] os: [linux] - '@bruits/satteri-linux-x64-gnu@0.9.5': - resolution: {integrity: sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw==} - cpu: [x64] - os: [linux] - '@bruits/satteri-linux-x64-musl@0.10.5': resolution: {integrity: sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==} cpu: [x64] os: [linux] - '@bruits/satteri-linux-x64-musl@0.9.5': - resolution: {integrity: sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q==} - cpu: [x64] - os: [linux] - '@bruits/satteri-wasm32-wasi@0.10.5': resolution: {integrity: sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@bruits/satteri-wasm32-wasi@0.9.5': - resolution: {integrity: sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - '@bruits/satteri-win32-arm64-msvc@0.10.5': resolution: {integrity: sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==} cpu: [arm64] os: [win32] - '@bruits/satteri-win32-arm64-msvc@0.9.5': - resolution: {integrity: sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg==} - cpu: [arm64] - os: [win32] - '@bruits/satteri-win32-x64-msvc@0.10.5': resolution: {integrity: sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==} cpu: [x64] os: [win32] - '@bruits/satteri-win32-x64-msvc@0.9.5': - resolution: {integrity: sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g==} - cpu: [x64] - os: [win32] - '@capsizecss/unpack@4.0.1': resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} engines: {node: '>=18'} @@ -2021,15 +1976,6 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@rollup/pluginutils@5.4.0': - resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/rollup-android-arm-eabi@4.63.1': resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} cpu: [arm] @@ -2695,12 +2641,12 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - astro@7.0.9: - resolution: {integrity: sha512-WB5pA4LLQnmqjBh6EIu0z8aUV4q2/AoThgSZq57Rsp+oqqvPu7OwZ5eF+W4ku20TUTxIhiJW8dccuGvJPiW2UA==} + astro@7.3.2: + resolution: {integrity: sha512-ysTcdpGP61XZpHoMRYC/CK19DQ94qkBvzXMtXEo0gEqPNkmOU/Tv++CtWmzaI4nF2Ck8RXFdiWvdlTWa2oOr9w==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: - '@astrojs/markdown-remark': 7.2.1 + '@astrojs/markdown-remark': ^7.3.0 peerDependenciesMeta: '@astrojs/markdown-remark': optional: true @@ -2823,6 +2769,10 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -3059,8 +3009,8 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} dom-serializer@2.0.0: @@ -3175,9 +3125,6 @@ packages: estree-util-visit@2.0.0: resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -3232,6 +3179,10 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + find-proc@0.1.0: + resolution: {integrity: sha512-OaOpEYv2PiQ7SQ5LIrl+deA1XaWcxEjnpM6VuWXTUvn+teIXxeFTLDmu18/zDQpFmHN4o3oDBX+BT0AGwEhemg==} + engines: {node: ^20.19.0 || >=22.12.0} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -3682,6 +3633,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + magicast@0.5.4: resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} @@ -3917,8 +3871,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - neotraverse@0.6.18: - resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + neotraverse@1.0.1: + resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==} engines: {node: '>= 10'} nlcst-to-string@4.0.0: @@ -4323,9 +4277,6 @@ packages: satteri@0.10.5: resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==} - satteri@0.9.5: - resolution: {integrity: sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w==} - sax@1.6.1: resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} @@ -5043,25 +4994,25 @@ snapshots: - prettier - prettier-plugin-astro - '@astrojs/compiler-binding-darwin-arm64@0.3.2': + '@astrojs/compiler-binding-darwin-arm64@0.4.0': optional: true - '@astrojs/compiler-binding-darwin-x64@0.3.2': + '@astrojs/compiler-binding-darwin-x64@0.4.0': optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': + '@astrojs/compiler-binding-linux-arm64-gnu@0.4.0': optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': + '@astrojs/compiler-binding-linux-arm64-musl@0.4.0': optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': + '@astrojs/compiler-binding-linux-x64-gnu@0.4.0': optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.3.2': + '@astrojs/compiler-binding-linux-x64-musl@0.4.0': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@astrojs/compiler-binding-wasm32-wasi@0.4.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': dependencies: '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) transitivePeerDependencies: @@ -5069,30 +5020,30 @@ snapshots: - '@emnapi/runtime' optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': + '@astrojs/compiler-binding-win32-arm64-msvc@0.4.0': optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': + '@astrojs/compiler-binding-win32-x64-msvc@0.4.0': optional: true - '@astrojs/compiler-binding@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@astrojs/compiler-binding@0.4.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.3.2 - '@astrojs/compiler-binding-darwin-x64': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-musl': 0.3.2 - '@astrojs/compiler-binding-linux-x64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-x64-musl': 0.3.2 - '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) - '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.2 - '@astrojs/compiler-binding-win32-x64-msvc': 0.3.2 + '@astrojs/compiler-binding-darwin-arm64': 0.4.0 + '@astrojs/compiler-binding-darwin-x64': 0.4.0 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.4.0 + '@astrojs/compiler-binding-linux-arm64-musl': 0.4.0 + '@astrojs/compiler-binding-linux-x64-gnu': 0.4.0 + '@astrojs/compiler-binding-linux-x64-musl': 0.4.0 + '@astrojs/compiler-binding-wasm32-wasi': 0.4.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/compiler-binding-win32-arm64-msvc': 0.4.0 + '@astrojs/compiler-binding-win32-x64-msvc': 0.4.0 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@astrojs/compiler-rs@0.4.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': dependencies: - '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/compiler-binding': 0.4.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -5110,7 +5061,7 @@ snapshots: smol-toml: 1.8.0 unified: 11.0.5 - '@astrojs/internal-helpers@0.10.1': + '@astrojs/internal-helpers@0.10.4': dependencies: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 @@ -5121,7 +5072,7 @@ snapshots: smol-toml: 1.8.0 unified: 11.0.5 - '@astrojs/internal-helpers@0.10.4': + '@astrojs/internal-helpers@0.11.0': dependencies: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 @@ -5179,28 +5130,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/markdown-satteri@0.3.4': + '@astrojs/markdown-satteri@0.3.8': dependencies: - '@astrojs/internal-helpers': 0.10.1 + '@astrojs/internal-helpers': 0.10.4 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 - hast-util-from-html: 2.0.3 - satteri: 0.9.5 + satteri: 0.10.5 - '@astrojs/markdown-satteri@0.3.8': + '@astrojs/markdown-satteri@0.4.1': dependencies: - '@astrojs/internal-helpers': 0.10.4 + '@astrojs/internal-helpers': 0.11.0 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 satteri: 0.10.5 - '@astrojs/mdx@7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0))': + '@astrojs/mdx@7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.4 '@astrojs/markdown-remark': 7.2.4 '@mdx-js/mdx': 3.1.1 acorn: 8.18.0 - astro: 7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0) + astro: 7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0) es-module-lexer: 2.3.2 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -5381,39 +5331,21 @@ snapshots: '@bruits/satteri-darwin-arm64@0.10.5': optional: true - '@bruits/satteri-darwin-arm64@0.9.5': - optional: true - '@bruits/satteri-darwin-x64@0.10.5': optional: true - '@bruits/satteri-darwin-x64@0.9.5': - optional: true - '@bruits/satteri-linux-arm64-gnu@0.10.5': optional: true - '@bruits/satteri-linux-arm64-gnu@0.9.5': - optional: true - '@bruits/satteri-linux-arm64-musl@0.10.5': optional: true - '@bruits/satteri-linux-arm64-musl@0.9.5': - optional: true - '@bruits/satteri-linux-x64-gnu@0.10.5': optional: true - '@bruits/satteri-linux-x64-gnu@0.9.5': - optional: true - '@bruits/satteri-linux-x64-musl@0.10.5': optional: true - '@bruits/satteri-linux-x64-musl@0.9.5': - optional: true - '@bruits/satteri-wasm32-wasi@0.10.5': dependencies: '@emnapi/core': 1.11.1 @@ -5421,25 +5353,12 @@ snapshots: '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@bruits/satteri-wasm32-wasi@0.9.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - optional: true - '@bruits/satteri-win32-arm64-msvc@0.10.5': optional: true - '@bruits/satteri-win32-arm64-msvc@0.9.5': - optional: true - '@bruits/satteri-win32-x64-msvc@0.10.5': optional: true - '@bruits/satteri-win32-x64-msvc@0.9.5': - optional: true - '@capsizecss/unpack@4.0.1': dependencies: fontkitten: 1.0.3 @@ -5473,10 +5392,10 @@ snapshots: '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/nimbus-docs@0.13.1(patch_hash=291b76e22c5f035fd612c4f8086e9026262c2ecbe5af3de58a9603413f4838b0)(@readme/httpsnippet@11.4.0)(@scalar/openapi-parser@0.28.12)(@types/node@24.13.3)(astro@7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0))(esbuild@0.28.2)(jiti@2.7.0)(openapi-sampler@1.7.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@cloudflare/nimbus-docs@0.13.1(patch_hash=291b76e22c5f035fd612c4f8086e9026262c2ecbe5af3de58a9603413f4838b0)(@readme/httpsnippet@11.4.0)(@scalar/openapi-parser@0.28.12)(@types/node@24.13.3)(astro@7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))(esbuild@0.28.2)(jiti@2.7.0)(openapi-sampler@1.7.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@astrojs/markdown-satteri': 0.3.8 - '@astrojs/mdx': 7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0)) + '@astrojs/mdx': 7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.2 '@clack/prompts': 0.9.1 '@iconify/tools': 5.0.14 @@ -5485,7 +5404,7 @@ snapshots: '@shikijs/transformers': 4.4.3 '@shikijs/types': 4.4.3 '@vercel/detect-agent': 1.2.3 - astro: 7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0) + astro: 7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0) clsx: 2.1.1 giget: 3.3.1 github-slugger: 2.0.0 @@ -6836,14 +6755,6 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/pluginutils@5.4.0(rollup@4.63.1)': - dependencies: - '@types/estree': 1.0.9 - estree-walker: 2.0.2 - picomatch: 4.0.7 - optionalDependencies: - rollup: 4.63.1 - '@rollup/rollup-android-arm-eabi@4.63.1': optional: true @@ -7512,28 +7423,28 @@ snapshots: astring@1.9.0: {} - astro@7.0.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(rollup@4.63.1)(yaml@2.9.0): + astro@7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler-rs': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) - '@astrojs/internal-helpers': 0.10.1 - '@astrojs/markdown-satteri': 0.3.4 + '@astrojs/compiler-rs': 0.4.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/internal-helpers': 0.11.0 + '@astrojs/markdown-satteri': 0.4.1 '@astrojs/telemetry': 3.3.3 '@capsizecss/unpack': 4.0.1 '@clack/prompts': 1.7.0 '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.4.0(rollup@4.63.1) am-i-vibing: 0.4.0 aria-query: 5.3.2 axobject-query: 4.1.0 ci-info: 4.4.0 clsx: 2.1.1 common-ancestor-path: 2.0.0 - cookie: 1.1.1 + cookie: 2.0.1 devalue: 5.9.2 - diff: 8.0.4 + diff: 9.0.0 dset: 3.1.4 es-module-lexer: 2.3.2 esbuild: 0.28.2 + find-proc: 0.1.0 flattie: 1.1.1 fontace: 0.4.1 get-tsconfig: 5.0.0-beta.4 @@ -7542,10 +7453,10 @@ snapshots: http-cache-semantics: 4.2.0 js-yaml: 4.3.2 jsonc-parser: 3.3.1 - magic-string: 0.30.21 + magic-string: 1.2.3 magicast: 0.5.4 mrmime: 2.0.1 - neotraverse: 0.6.18 + neotraverse: 1.0.1 obug: 2.1.4 p-limit: 7.3.2 p-queue: 9.3.3 @@ -7594,7 +7505,6 @@ snapshots: - ioredis - jiti - less - - rollup - sass - sass-embedded - stylus @@ -7694,6 +7604,8 @@ snapshots: cookie@1.1.1: {} + cookie@2.0.1: {} + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -7954,7 +7866,7 @@ snapshots: dependencies: dequal: 2.0.3 - diff@8.0.4: {} + diff@9.0.0: {} dom-serializer@2.0.0: dependencies: @@ -8130,8 +8042,6 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 3.0.3 - estree-walker@2.0.2: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -8192,6 +8102,8 @@ snapshots: fflate@0.8.3: {} + find-proc@0.1.0: {} + flattie@1.1.1: {} fontace@0.4.1: @@ -8641,6 +8553,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + magic-string@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + magicast@0.5.4: dependencies: '@babel/parser': 7.29.8 @@ -9157,7 +9073,7 @@ snapshots: nanoid@3.3.18: {} - neotraverse@0.6.18: {} + neotraverse@1.0.1: {} nlcst-to-string@4.0.0: dependencies: @@ -9722,23 +9638,6 @@ snapshots: '@bruits/satteri-win32-arm64-msvc': 0.10.5 '@bruits/satteri-win32-x64-msvc': 0.10.5 - satteri@0.9.5: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.5 - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - optionalDependencies: - '@bruits/satteri-darwin-arm64': 0.9.5 - '@bruits/satteri-darwin-x64': 0.9.5 - '@bruits/satteri-linux-arm64-gnu': 0.9.5 - '@bruits/satteri-linux-arm64-musl': 0.9.5 - '@bruits/satteri-linux-x64-gnu': 0.9.5 - '@bruits/satteri-linux-x64-musl': 0.9.5 - '@bruits/satteri-wasm32-wasi': 0.9.5 - '@bruits/satteri-win32-arm64-msvc': 0.9.5 - '@bruits/satteri-win32-x64-msvc': 0.9.5 - sax@1.6.1: {} scheduler@0.27.0: {} diff --git a/src/README.md b/src/README.md index f01c1059c..cfdd64c7a 100644 --- a/src/README.md +++ b/src/README.md @@ -10,8 +10,8 @@ Mintlify-flavoured MDX build (see `src/plugins/vite-mintlify-snippets.ts` and | Command | What it does | |---|---| | `pnpm install` | Node 24, pnpm 10. | -| `pnpm build` | Fetches registered sources, builds the selected locale scope, then prunes `.mdx` twins, rebases URLs, generates `__redirects`, nests under `dist/docs`, and enforces the Worker asset limits. | -| `pnpm run build:vercel` | Fetches registered sources using Vercel Connect where required, removes the deployment OIDC identity, then builds the Vercel output. | +| `pnpm build` | Fetches registered sources, builds the selected locale scope, rebases URLs, generates `__redirects`, nests under `dist/docs`, and enforces the Worker asset limits. | +| `pnpm run build:vercel` | Fetches registered sources using Vercel Connect where required, removes the deployment OIDC identity, builds English and requested locales in isolated sequential Astro processes, then merges one Vercel output. | | `pnpm dev` | Astro dev server (`/docs/...`). | | `pnpm check:mdx` | Compiles every MDX file with SΓ€tteri and reports undefined components; seconds, no build. | | `pnpm measure` | Page weight, anchor parity, base-path check, URL parity vs the live Mintlify sitemap (needs a nested build in `$DOCS_OUT_DIR`). | @@ -25,7 +25,7 @@ Mintlify-flavoured MDX build (see `src/plugins/vite-mintlify-snippets.ts` and | Variable | Effect | |---|---| | `DOCS_INCLUDE` | Comma-separated globs restricting the English collection (spikes, scoped previews). | -| `DOCS_LOCALE` | The one locale Worker to build (`en`, `es`, `pt-BR`, and so on); unset = English. | +| `DOCS_LOCALE` | A singular locale build (`en`, `es`, `pt-BR`, and so on); normally set only by the Vercel shard orchestrator. | | `DOCS_LOCALES` | Translations to add to the English Vercel artifact: `none`, `all`, or a comma-separated list such as `es,fr`. Vercel production always builds `all`. | | `DOCS_REMOTES` | Registered remote-source scope: `none` for a base-repository preview and `all` for source previews and production. | | `DOCS_REMOTE_NAME`, `DOCS_REMOTE_REPOSITORY`, `DOCS_REMOTE_REF` | CI-only tuple selecting one registered remote at an immutable commit for an English pull-request preview. | @@ -33,8 +33,8 @@ Mintlify-flavoured MDX build (see `src/plugins/vite-mintlify-snippets.ts` and | `DOCS_REMOTES_PREFETCHED=1` | Requires the remote mounts and fetch-state files supplied by the credentialed CI fetch job. | | `DOCS_PREVIEW_ALIAS` | Lowercase Cloudflare alias used by `pnpm run deploy:preview`. | | `DOCS_GITHUB_CONNECTOR` | Vercel Connect GitHub connector UID, for example `github/clickhouse-docs`. Configure it only for `production` and the `connect-preview` Custom Environment. | -| `DOCS_OUT_DIR`, `DOCS_CACHE_DIR` | Isolated output and cache directories (parallel builds never share `dist/`). | -| `NODE_OPTIONS=--max-old-space-size=8192` | Recommended for full builds (peak RSS ~3 GB). | +| `DOCS_OUT_DIR`, `DOCS_CACHE_DIR` | Isolated output and cache directories. Vercel uses one persistent Astro cache per locale under `node_modules/.astro/`. | +| `NODE_OPTIONS=--max-old-space-size=8192` | Recommended for full builds. Locale processes run sequentially, so memory is bounded to one content tree at a time. | ## Layout @@ -42,7 +42,7 @@ Mintlify-flavoured MDX build (see `src/plugins/vite-mintlify-snippets.ts` and - `src/content.config.ts`: `docs` (English, path-derived ids) and one collection per locale (`es`, `pt-br`, ...). - `src/pages/[...slug].astro`, `src/pages/[locale]/[...slug].astro`: page routes (locale pages fall back to English). - `src/pages/nav/[...key].astro`: lazy sidebar fragments; `src/lib/sidebar-lazy.ts`. -- `src/pages/**/llms*.txt.ts`, `**/index.md.ts`: agent surfaces. The root `llms-full.txt` links to full-text, top-level-section `llms.txt` files; `src/lib/corpus.ts` recursively subdivides any corpus that reaches 24 MiB. +- `src/pages/[...slug].md.ts`, `src/pages/**/llms*.txt.ts`: English-only agent surfaces. Every human-language route points to the same canonical English `.md`; no generated `.mdx` or localized agent copies are emitted. The root `llms-full.txt` links to full-text, top-level-section `llms.txt` files; `src/lib/corpus.ts` recursively subdivides any corpus that reaches 24 MiB. - `src/components/compat/`: Mintlify component names on Nimbus components; `react/` shims for snippet JSX. - `bin/`: generators and measurement scripts; `worker/`: Cloudflare Worker; `wrangler.jsonc`. - `src/generated/` (gitignored): sidebar items, import index, island wrappers. @@ -97,7 +97,10 @@ more locale labels such as `docs-translations-es` and preview with the resulting locale set. `.github/workflows/site-production.yml` asks Vercel to fetch the merged `main` commit through the same Git connection and build English with every translation. Both workflows can also be invoked -manually from the default branch. +manually from the default branch. `bin/vercel-build.ts` compiles English and +each requested locale in its own sequential Astro child process, preserving a +separate incremental cache for each one, and merges the locale routes plus +their namespaced assets into one deployment. Vercel must be provisioned as follows: @@ -122,6 +125,7 @@ Vercel must be provisioned as follows: 9. Keep the Vercel build command as `pnpm run build:vercel` and the output directory as `dist`. -The website Worker routes `/docs//*` and -`/docs/_astro-/*` to `clickhouse-docs-`. The English Worker -handles the remaining `/docs/*` paths, including shared images and Nimbus assets. +The single Vercel project serves English and every selected locale. English +owns shared public files and `/docs/_astro`; each locale contributes only +`/docs/` and its `/docs/_astro-` asset namespace to the merged +artifact. diff --git a/src/components/api/ApiPage.astro b/src/components/api/ApiPage.astro index 42e403f58..83d56dcb4 100644 --- a/src/components/api/ApiPage.astro +++ b/src/components/api/ApiPage.astro @@ -37,7 +37,7 @@ const headings = [ ...(view.body.length || view.bodyUnion ? [{ depth: 2, text: "Request body", slug: "request-body" }] : []), ...(view.responses.length ? [{ depth: 2, text: "Response", slug: "response" }] : []), ]; -const markdownPath = withBase(`/${source.route}/index.md`); +const markdownPath = withBase(`/${source.route}.md`); const markdownUrl = Astro.site ? new URL(markdownPath, Astro.site).href : markdownPath; --- diff --git a/src/content.config.ts b/src/content.config.ts index afd81cb13..9001710c1 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -131,11 +131,15 @@ export type LocaleCollectionName = "ar" | "es" | "fr" | "ja" | "ko" | "pt-br" | /** * Locales come from the build scope (`DOCS_LOCALES` for a combined Vercel - * artifact, `DOCS_LOCALE` for one locale Worker, or `.preview-scope.json` for + * artifact, `DOCS_LOCALE` for one locale shard, or `.preview-scope.json` for * remote previews; see src/lib/scope.ts). Collection names deliberately avoid * the `docs-` prefix, which Nimbus reserves for versions. */ export const ACTIVE_LOCALES: string[] = [...scope.locales]; +/** Locales present in the final merged artifact and exposed by language links. */ +export const AVAILABLE_LOCALES: string[] = [...scope.availableLocales]; +/** English routes are emitted only by the English build shard. */ +export const EMIT_ENGLISH = scope.emitEnglish; /** * Collection names are lowercase even when the canonical URL segment is not. @@ -168,14 +172,18 @@ const partialSchema = z.object({ export const collections = { docs: defineCollection({ - loader: withNimbusMarkdown(glob({ base: ".", pattern: treePattern("."), generateId: pathId })), + loader: withNimbusMarkdown(glob({ + base: ".", + pattern: EMIT_ENGLISH ? treePattern(".") : "__inactive_english__/**/*.{md,mdx}", + generateId: pathId, + })), // Non-strict: the content carries Docusaurus-era keys we do not model. schema, }), changelog: defineCollection({ loader: withNimbusMarkdown(glob({ base: ".remote/changelog", - pattern: scope.remotePreview ? "__remote_preview_excludes_changelog__/**/*.mdx" : "**/*.mdx", + pattern: !EMIT_ENGLISH || scope.remotePreview ? "__inactive_changelog__/**/*.mdx" : "**/*.mdx", generateId: pathId, })), schema: changelogSchema, diff --git a/src/lib/corpus.ts b/src/lib/corpus.ts index f66f6c02d..7957e06b6 100644 --- a/src/lib/corpus.ts +++ b/src/lib/corpus.ts @@ -119,7 +119,8 @@ export function renderCorpus(entries: IndexedEntry[], title: string, indexPath: } function markdownUrl(entry: IndexedEntry): string { - return new URL(withBase(`${entry.url.replace(/\/+$/, "")}/index.md`), config.site).href; + const route = entry.url.replace(/\/+$/, ""); + return new URL(withBase(route === "/" ? "/index.md" : `${route}.md`), config.site).href; } function corpusUrl(pathname: string): string { diff --git a/src/lib/locale-content.server.ts b/src/lib/locale-content.server.ts new file mode 100644 index 000000000..1b23ff741 --- /dev/null +++ b/src/lib/locale-content.server.ts @@ -0,0 +1,21 @@ +import fs from "node:fs"; +import path from "node:path"; + +/** Content ids that have an authored translation for one locale. */ +export function translatedPageIds(locale: string, sections: readonly string[]): Set { + const ids = new Set(); + const localeRoot = path.join(process.cwd(), locale); + const visit = (directory: string): void => { + if (!fs.existsSync(directory)) return; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) visit(file); + else if (/\.mdx?$/.test(entry.name)) { + const relative = path.relative(localeRoot, file).replaceAll(path.sep, "/"); + ids.add(relative.replace(/\.mdx?$/, "").replace(/\/index$/, "")); + } + } + }; + for (const section of sections) visit(path.join(localeRoot, section)); + return ids; +} diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts index ad1fef76e..55723bba4 100644 --- a/src/lib/openapi.ts +++ b/src/lib/openapi.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { createHash } from "node:crypto"; import { buildApiModel, getApiPageProps, @@ -103,6 +104,18 @@ export interface ApiOperationPage { badge?: string; } +const documentDigestCache = new WeakMap(); + +/** Invalidate every operation when its shared OpenAPI document changes. */ +export function apiPageCacheKey(page: ApiOperationPage): string { + let digest = documentDigestCache.get(page.document); + if (!digest) { + digest = createHash("sha256").update(JSON.stringify(page.document)).digest("hex"); + documentDigestCache.set(page.document, digest); + } + return `${digest}:${page.method}:${page.path}`; +} + const COLLECTIONS: Record = { cloud: { file: ".remote/specs/cloud-openapi.json", @@ -287,7 +300,7 @@ export async function getRoutedApiOperation( view: { ...page, href, - markdownHref: `${href}/index.md`, + markdownHref: `${href}.md`, }, }; } diff --git a/src/lib/scope.ts b/src/lib/scope.ts index 9be18a67e..a52c726c8 100644 --- a/src/lib/scope.ts +++ b/src/lib/scope.ts @@ -1,11 +1,11 @@ /** - * Build scope for Vercel's combined site and independently deployed locale - * Workers. + * Build scope for Vercel's combined site and its isolated locale build shards. * * `DOCS_LOCALES` keeps English active and adds zero, one, several, or every * translated collection to the same Vercel artifact. Vercel production always * builds every translation, even when the variable is omitted. `DOCS_LOCALE` - * retains the singular build contract used by locale Workers. + * selects one internal child build; `bin/vercel-build.ts` runs those children + * sequentially and merges their outputs. * * { * "locale": "en", @@ -36,10 +36,14 @@ export interface RemotePreview { } export interface BuildScope { - /** Primary locale; non-English only for a singular locale Worker build. */ + /** Primary locale; non-English only inside a locale build shard. */ locale: BuildLocale; /** Non-English collections included in this artifact. */ locales: Locale[]; + /** Locales linked by the shared chrome, even when built in another shard. */ + availableLocales: Locale[]; + /** Whether this shard emits the English routes and English-only surfaces. */ + emitEnglish: boolean; /** Whether `reference/**` is part of the build. */ reference: boolean; /** Whether registered remote sources participate in this build. */ @@ -150,6 +154,8 @@ function parseRemotes(value: unknown, source: string): boolean { export function readScope(root = process.cwd()): BuildScope { const envLocale = (process.env.DOCS_LOCALE ?? "").trim(); const envLocales = (process.env.DOCS_LOCALES ?? "").trim(); + const envAvailableLocales = (process.env.DOCS_AVAILABLE_LOCALES ?? "").trim(); + const envEmitEnglish = (process.env.DOCS_EMIT_ENGLISH ?? "").trim(); const envRemotes = (process.env.DOCS_REMOTES ?? "").trim(); const envReference = (process.env.DOCS_REFERENCE ?? "").trim().toLowerCase(); const envRemoteName = (process.env.DOCS_REMOTE_NAME ?? "").trim(); @@ -183,6 +189,7 @@ export function readScope(root = process.cwd()): BuildScope { .trim() .toLowerCase(); const isVercelProduction = process.env.VERCEL === "1" && vercelTarget === "production"; + const isBuildShard = process.env.DOCS_BUILD_SHARD === "1"; const locale = envLocales ? "en" @@ -197,7 +204,7 @@ export function readScope(root = process.cwd()): BuildScope { ? [] : [locale]; - if (isVercelProduction) { + if (isVercelProduction && !isBuildShard) { if (locale !== "en") { throw new Error("Vercel production builds use DOCS_LOCALES=all, not DOCS_LOCALE"); } @@ -206,6 +213,15 @@ export function readScope(root = process.cwd()): BuildScope { } locales = [...ALL_LOCALES]; } + const availableLocales = envAvailableLocales + ? parseLocales(envAvailableLocales, "DOCS_AVAILABLE_LOCALES") + : [...locales]; + const emitEnglish = envEmitEnglish + ? parseReference(envEmitEnglish, "DOCS_EMIT_ENGLISH") + : locale === "en"; + if (!emitEnglish && locale === "en") { + throw new Error("DOCS_EMIT_ENGLISH=false requires a non-English DOCS_LOCALE shard"); + } const reference = envReference ? parseReference(envReference, "DOCS_REFERENCE") : fileScope?.reference !== undefined @@ -237,10 +253,12 @@ export function readScope(root = process.cwd()): BuildScope { if (isVercelProduction && !remotes) { throw new Error("Vercel production builds require DOCS_REMOTES=all"); } - if (envLocale || envLocales || envRemotes || envReference || hasRemoteEnvironment) source = "env"; + if (envLocale || envLocales || envAvailableLocales || envEmitEnglish || envRemotes || envReference || hasRemoteEnvironment) source = "env"; return { locale, locales, + availableLocales, + emitEnglish, reference, remotes, remotePreview, diff --git a/src/lib/sidebar-lazy.ts b/src/lib/sidebar-lazy.ts index e22b2ec86..f5fd48308 100644 --- a/src/lib/sidebar-lazy.ts +++ b/src/lib/sidebar-lazy.ts @@ -41,6 +41,74 @@ function siblingKeys(labels: string[]): string[] { export type LazyGroup = { key: string; label: string; items: ConfigItem[]; path: string[] }; +type ConfigGroup = Extract; +type NavigationLocation = { + tabIndex: number; + railItems: ConfigItem[]; + railPath: string[]; +}; +type NavigationIndex = { + locations: Map; + tabs: ConfigGroup[]; + firstLinks: string[]; +}; + +const navigationIndexes = new WeakMap(); +const normPath = (value: string): string => value.replace(/\/+$/, "") || "/"; + +function internalLinks(nodes: ConfigItem[]): string[] { + const links: string[] = []; + for (const node of nodes) { + if ("items" in node) links.push(...internalLinks(node.items)); + else if (!/^(https?:)?\/\//.test(node.link)) links.push(normPath(withBase(node.link))); + } + return links; +} + +/** Index every page's rail selection once instead of rescanning the tree per page. */ +function navigationIndex(items: ConfigItem[]): NavigationIndex { + const cached = navigationIndexes.get(items); + if (cached) return cached; + + const tabs = items.filter((item): item is ConfigGroup => "items" in item); + const locations = new Map(); + tabs.forEach((tab, tabIndex) => { + const tabLocation = { tabIndex, railItems: tab.items, railPath: [] }; + for (const link of internalLinks(tab.items)) locations.set(link, tabLocation); + + const topGroups = tab.items.filter((item): item is ConfigGroup => "items" in item); + const topKeys = siblingKeys(topGroups.map((group) => group.label)); + topGroups.forEach((topGroup, topIndex) => { + const topLocation = { + tabIndex, + railItems: topGroup.items, + railPath: [topKeys[topIndex]], + }; + for (const link of internalLinks(topGroup.items)) locations.set(link, topLocation); + + if (tab.label !== "Solutions") return; + const productGroups = topGroup.items.filter((item): item is ConfigGroup => "items" in item); + const productKeys = siblingKeys(productGroups.map((group) => group.label)); + productGroups.forEach((productGroup, productIndex) => { + const productLocation = { + tabIndex, + railItems: productGroup.items, + railPath: [topKeys[topIndex], productKeys[productIndex]], + }; + for (const link of internalLinks(productGroup.items)) locations.set(link, productLocation); + }); + }); + }); + + const index = { + locations, + tabs, + firstLinks: tabs.map((tab) => internalLinks(tab.items)[0] ?? "/"), + }; + navigationIndexes.set(items, index); + return index; +} + /** Every group in the config tree with its fragment key. Tabs are skipped. */ export function collectGroups(items: ConfigItem[], path: string[] = [], out: LazyGroup[] = [], depth = 0): LazyGroup[] { const groups = items.filter((i): i is Extract => "items" in i); @@ -108,44 +176,19 @@ export function assignLazyKeys(items: T[], path: string[] * lazy fragments (`/nav//...`). */ export function buildRailFromConfig(items: ConfigItem[], currentPath: string, keyPrefix: string[] = []): SidebarItem[] { - const norm = (p: string) => p.replace(/\/+$/, "") || "/"; - const target = norm(currentPath); - const tabs = items.filter((i): i is Extract => "items" in i); - const contains = (nodes: ConfigItem[]): boolean => - nodes.some((n) => ("items" in n ? contains(n.items) : norm(withBase(n.link)) === target)); - const tab = tabs.find((t) => contains(t.items)) ?? tabs[0]; - if (!tab) return []; - - const groupsWithKeys = (nodes: ConfigItem[]) => { - const groups = nodes.filter((i): i is Extract => "items" in i); - const keys = siblingKeys(groups.map((group) => group.label)); - return groups.map((group, index) => ({ group, key: keys[index] })); - }; - - // Database, Integrations, and Resources expose their direct groups in the - // top menu. Solutions adds a presentational section level (ClickHouse Cloud - // and Open source), so select its actual product entry one level deeper. - const topMatch = groupsWithKeys(tab.items).find(({ group }) => contains(group.items)); - let railItems = tab.items; - let railPath = keyPrefix; - if (topMatch) { - railItems = topMatch.group.items; - railPath = [...keyPrefix, topMatch.key]; - if (tab.label === "Solutions") { - const productMatch = groupsWithKeys(topMatch.group.items).find(({ group }) => contains(group.items)); - if (productMatch) { - railItems = productMatch.group.items; - railPath = [...railPath, productMatch.key]; - } - } - } + const target = normPath(currentPath); + const index = navigationIndex(items); + const location = index.locations.get(target); + const railItems = location?.railItems ?? index.tabs[0]?.items; + if (!railItems) return []; + const railPath = [...keyPrefix, ...(location?.railPath ?? [])]; const rendered = toRendered(railItems, railPath); const mark = (nodes: SidebarItem[]): boolean => { let any = false; for (const n of nodes) { if (n.type === "link") { - if (norm(n.href) === target) { (n as { isCurrent?: boolean }).isCurrent = true; any = true; } + if (normPath(n.href) === target) { (n as { isCurrent?: boolean }).isCurrent = true; any = true; } } else if (n.type === "group") { const hit = mark(n.children); if (hit) { n.collapsed = false; any = true; } @@ -159,18 +202,12 @@ export function buildRailFromConfig(items: ConfigItem[], currentPath: string, ke /** Top-level sections (tabs) for the header, from the generated config tree. */ export function sectionsFromConfig(items: ConfigItem[], currentPath: string): Array<{ label: string; href: string; isActive: boolean }> { - const norm = (p: string) => p.replace(/\/+$/, "") || "/"; - const target = norm(currentPath); - const firstLink = (nodes: ConfigItem[]): string | undefined => { - for (const n of nodes) { - if ("items" in n) { const l = firstLink(n.items); if (l) return l; } - else if (!/^(https?:)?\/\//.test(n.link)) return n.link; - } - return undefined; - }; - const contains = (nodes: ConfigItem[]): boolean => - nodes.some((n) => ("items" in n ? contains(n.items) : norm(withBase(n.link)) === target)); - return items - .filter((i): i is Extract => "items" in i) - .map((t) => ({ label: t.label, href: withBase(firstLink(t.items) ?? "/"), isActive: contains(t.items) })); + const target = normPath(currentPath); + const index = navigationIndex(items); + const activeTab = index.locations.get(target)?.tabIndex ?? -1; + return index.tabs.map((tab, tabIndex) => ({ + label: tab.label, + href: index.firstLinks[tabIndex], + isActive: tabIndex === activeTab, + })); } diff --git a/src/lib/sidebar-navigation.server.ts b/src/lib/sidebar-navigation.server.ts new file mode 100644 index 000000000..a70227f4e --- /dev/null +++ b/src/lib/sidebar-navigation.server.ts @@ -0,0 +1,30 @@ +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; + +import type { ConfigItem } from "./sidebar-lazy"; + +interface NavigationData { + items: ConfigItem[]; + digest: string; +} + +const cache = new Map(); + +/** Read and parse one generated navigation tree once for the entire build. */ +export function loadGeneratedNavigation(locale: string): NavigationData { + const key = locale.toLowerCase(); + const cached = cache.get(key); + if (cached) return cached; + + const localized = path.join(process.cwd(), "src/generated", `sidebar.items.${locale}.json`); + const english = path.join(process.cwd(), "src/generated", "sidebar.items.json"); + const file = fs.existsSync(localized) ? localized : english; + const source = fs.readFileSync(file, "utf8"); + const data = { + items: JSON.parse(source) as ConfigItem[], + digest: createHash("sha256").update(source).digest("hex"), + }; + cache.set(key, data); + return data; +} diff --git a/src/pages/[...corpus]/llms.txt.ts b/src/pages/[...corpus]/llms.txt.ts index 6d4704c0d..87e1deb98 100644 --- a/src/pages/[...corpus]/llms.txt.ts +++ b/src/pages/[...corpus]/llms.txt.ts @@ -1,5 +1,7 @@ import { config } from "virtual:nimbus/config"; import { buildCorpusShards, englishCorpusEntries } from "../../lib/corpus"; +import { EMIT_ENGLISH } from "../../content.config"; +import { createHash } from "node:crypto"; export const prerender = true; @@ -8,9 +10,11 @@ interface Props { } export async function getStaticPaths() { + if (!EMIT_ENGLISH) return []; return buildCorpusShards(await englishCorpusEntries(), config.title).map((shard) => ({ params: { corpus: shard.path }, props: { body: shard.body }, + cacheKey: createHash("sha256").update(shard.body).digest("hex"), })); } diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index 25a08d4fc..01b92b18f 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -13,7 +13,7 @@ import { import { components } from "../components"; import { withBase, stripBase } from "../lib/base"; import { buildRailFromConfig, type ConfigItem } from "../lib/sidebar-lazy"; -import { ACTIVE_LOCALES, localeCollectionName } from "../content.config"; +import { AVAILABLE_LOCALES, EMIT_ENGLISH, localeCollectionName } from "../content.config"; import { localeRouteName } from "../util/locales"; import { getRemoteEditUrl } from "../lib/remotes"; import navigation from "../generated/sidebar.items.json"; @@ -21,6 +21,7 @@ import type { GetStaticPaths } from "astro"; export const prerender = true; export const getStaticPaths: GetStaticPaths = async (options) => { + if (!EMIT_ENGLISH) return []; const paths = await getDocsStaticPaths(options); return paths.filter(({ params }) => { const slug = params.slug; @@ -69,17 +70,17 @@ const toc = tocOn && effectiveTocConfig !== false ? getTOC(headings, effectiveTo const layoutMode = entry.data.mode === "custom" || entry.data.mode === "wide" ? entry.data.mode : "doc"; const data = entry.data as { title?: string; sidebarTitle?: string }; const pageTitle = data.title ?? data.sidebarTitle ?? entry.id.split("/").pop() ?? entry.id; -const markdownPath = withBase(`/${entry.id}/index.md`); +const markdownPath = withBase(`/${entry.id}.md`); const markdownUrl = Astro.site ? new URL(markdownPath, Astro.site).href : markdownPath; // No per-page OG cards yet (canvaskit); fall back to the site image. const socialImage = entry.data.socialImage; // hreflang alternates for the translated (or English-fallback) copies of this page. const HREFLANG: Record = { "pt-br": "pt-BR" }; -const alternates = Astro.site && ACTIVE_LOCALES.length && entry.id !== "index" +const alternates = Astro.site && AVAILABLE_LOCALES.length && entry.id !== "index" ? [ { hreflang: "en", href: new URL(withBase(`/${entry.id}/`), Astro.site).href }, { hreflang: "x-default", href: new URL(withBase(`/${entry.id}/`), Astro.site).href }, - ...ACTIVE_LOCALES.map((l) => ({ hreflang: HREFLANG[localeCollectionName(l)] ?? l, href: new URL(withBase(`/${localeRouteName(l)}/${entry.id}/`), Astro.site!).href })), + ...AVAILABLE_LOCALES.map((l) => ({ hreflang: HREFLANG[localeCollectionName(l)] ?? l, href: new URL(withBase(`/${localeRouteName(l)}/${entry.id}/`), Astro.site!).href })), ] : []; --- @@ -107,7 +108,7 @@ const alternates = Astro.site && ACTIVE_LOCALES.length && entry.id !== "index" lang="en" alternates={alternates} currentLocale="en" - activeLocales={ACTIVE_LOCALES} + activeLocales={AVAILABLE_LOCALES} > diff --git a/src/pages/[...slug].md.ts b/src/pages/[...slug].md.ts index 35be373ec..eff4a37b8 100644 --- a/src/pages/[...slug].md.ts +++ b/src/pages/[...slug].md.ts @@ -1,4 +1,4 @@ -/** Convenience alias: `/.md` serves the same artifact as `//index.md`. */ +/** Canonical agent-readable representation for every English documentation page. */ import { getPreparedMarkdownArtifact, getPreparedMarkdownStaticPaths, @@ -6,6 +6,7 @@ import { } from "@cloudflare/nimbus-docs/build"; import { prepareAgentMarkdown } from "../lib/agent-links"; import { withBase } from "../lib/base"; +import { EMIT_ENGLISH } from "../content.config"; export const prerender = true; @@ -14,14 +15,11 @@ interface Props { } export async function getStaticPaths() { + if (!EMIT_ENGLISH) return []; return (await getPreparedMarkdownStaticPaths({ collection: "docs", surface: "markdown", - })).filter( - ({ props }) => - props.artifact.id !== "index" && - !props.artifact.id.startsWith("products/cloud/api-reference/"), - ); + })).filter(({ props }) => !props.artifact.id.startsWith("products/cloud/api-reference/")); } export async function GET({ props }: { props: Props }) { diff --git a/src/pages/[...slug].mdx.ts b/src/pages/[...slug].mdx.ts deleted file mode 100644 index 886b068bf..000000000 --- a/src/pages/[...slug].mdx.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** Convenience alias: `/.mdx` serves the same source as `//index.mdx`. */ -import { - getPreparedMarkdownArtifact, - getPreparedMarkdownStaticPaths, - type PreparedMarkdownReference, -} from "@cloudflare/nimbus-docs/build"; - -export const prerender = true; - -interface Props { - artifact: PreparedMarkdownReference; -} - -export async function getStaticPaths() { - return (await getPreparedMarkdownStaticPaths({ - collection: "docs", - surface: "source", - })).filter( - ({ props }) => - props.artifact.id !== "index" && - !props.artifact.id.startsWith("products/cloud/api-reference/"), - ); -} - -export async function GET({ props }: { props: Props }) { - const artifact = await getPreparedMarkdownArtifact(props.artifact); - return new Response(artifact.body, { - headers: { "Content-Type": artifact.mediaType }, - }); -} diff --git a/src/pages/[...slug]/index.md.ts b/src/pages/[...slug]/index.md.ts deleted file mode 100644 index 2eba1239e..000000000 --- a/src/pages/[...slug]/index.md.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - getPreparedMarkdownArtifact, - getPreparedMarkdownStaticPaths, - type PreparedMarkdownReference, -} from "@cloudflare/nimbus-docs/build"; -import { prepareAgentMarkdown } from "../../lib/agent-links"; -import { withBase } from "../../lib/base"; - -export const prerender = true; - -interface Props { - artifact: PreparedMarkdownReference; -} - -export async function getStaticPaths() { - return (await getPreparedMarkdownStaticPaths({ - collection: "docs", - surface: "markdown", - })).filter(({ props }) => !props.artifact.id.startsWith("products/cloud/api-reference/")); -} - -export async function GET({ props }: { props: Props }) { - const artifact = await getPreparedMarkdownArtifact(props.artifact); - const pagePath = withBase(artifact.id === "index" ? "/" : `/${artifact.id}/`); - return new Response(prepareAgentMarkdown(artifact.body, pagePath), { - headers: { "Content-Type": artifact.mediaType }, - }); -} diff --git a/src/pages/[...slug]/index.mdx.ts b/src/pages/[...slug]/index.mdx.ts deleted file mode 100644 index 3a4e0f29c..000000000 --- a/src/pages/[...slug]/index.mdx.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - getPreparedMarkdownArtifact, - getPreparedMarkdownStaticPaths, - type PreparedMarkdownReference, -} from "@cloudflare/nimbus-docs/build"; - -export const prerender = true; - -interface Props { - artifact: PreparedMarkdownReference; -} - -export async function getStaticPaths() { - return (await getPreparedMarkdownStaticPaths({ - collection: "docs", - surface: "source", - })).filter(({ props }) => !props.artifact.id.startsWith("products/cloud/api-reference/")); -} - -export async function GET({ props }: { props: Props }) { - const artifact = await getPreparedMarkdownArtifact(props.artifact); - return new Response(artifact.body, { - headers: { "Content-Type": artifact.mediaType }, - }); -} diff --git a/src/pages/[locale]/[...slug].astro b/src/pages/[locale]/[...slug].astro index 23e7a3f8d..49d1a073e 100644 --- a/src/pages/[locale]/[...slug].astro +++ b/src/pages/[locale]/[...slug].astro @@ -19,41 +19,60 @@ import { components } from "../../components"; import { getCollection } from "astro:content"; import { Aside } from "@/components/ui/aside"; import { withBase, stripBase } from "../../lib/base"; -import { buildRailFromConfig, sectionsFromConfig, type ConfigItem } from "../../lib/sidebar-lazy"; -import fs from "node:fs"; -import path from "node:path"; -import { ACTIVE_LOCALES, localeCollectionName, type LocaleCollectionName } from "../../content.config"; +import { buildRailFromConfig, sectionsFromConfig } from "../../lib/sidebar-lazy"; +import { + ACTIVE_LOCALES, + AVAILABLE_LOCALES, + EMIT_ENGLISH, + SECTIONS, + localeCollectionName, + type LocaleCollectionName, +} from "../../content.config"; import { localeRouteName } from "../../util/locales"; import { getRemoteEditUrl } from "../../lib/remotes"; +import { loadGeneratedNavigation } from "../../lib/sidebar-navigation.server"; +import { translatedPageIds } from "../../lib/locale-content.server"; export const prerender = true; export async function getStaticPaths() { const paths: Array<{ params: { locale: string; slug: string | undefined }; props: unknown; cacheKey?: string }> = []; - const english = await getCollection("docs"); for (const sourceLocale of ACTIVE_LOCALES) { const collection = localeCollectionName(sourceLocale); const locale = localeRouteName(sourceLocale); + const navigationDigest = loadGeneratedNavigation(sourceLocale).digest; const localePaths = (await getCollectionStaticPaths(collection)({} as never)) as Array<{ params: { slug?: string }; - props: { entry: { id: string } }; + props: { entry: { id: string; digest?: string } }; cacheKey?: string; }>; - const translated = new Set(); for (const p of localePaths) { - translated.add(p.props.entry.id); - paths.push({ params: { locale, slug: p.params.slug }, props: p.props, cacheKey: p.cacheKey }); - } - // English fallback: every English page exists under every locale prefix. - for (const e of english) { - if (translated.has(e.id) || e.id === "index") continue; paths.push({ - params: { locale, slug: e.id }, - props: { entry: e, fallback: true }, - cacheKey: `fallback:${String((e as { digest?: string }).digest ?? e.id)}`, + params: { locale, slug: p.params.slug }, + props: p.props, + cacheKey: `${p.cacheKey ?? p.props.entry.digest ?? p.props.entry.id}:nav:${navigationDigest}`, }); } } + + // The English shard alone emits missing-translation fallbacks. Locale + // shards therefore compile only their translated collection, never English. + if (EMIT_ENGLISH && AVAILABLE_LOCALES.length) { + const english = await getCollection("docs"); + for (const sourceLocale of AVAILABLE_LOCALES) { + const locale = localeRouteName(sourceLocale); + const translated = translatedPageIds(sourceLocale, SECTIONS); + const navigationDigest = loadGeneratedNavigation(sourceLocale).digest; + for (const entry of english) { + if (translated.has(entry.id) || entry.id === "index") continue; + paths.push({ + params: { locale, slug: entry.id }, + props: { entry, fallback: true }, + cacheKey: `fallback:${String(entry.digest ?? entry.id)}:nav:${navigationDigest}`, + }); + } + } + } return paths; } @@ -69,9 +88,7 @@ const { sidebar: sidebarOn, tableOfContents: tocOn } = await getRouteFlags(entry // Locale rail: from the locale's generated navigation (labels and // // links), falling back to the English tree when a locale file is absent. -const localeItemsFile = path.join(process.cwd(), "src/generated", `sidebar.items.${ACTIVE_LOCALES.find((l) => localeCollectionName(l) === localeCode) ?? localeParam}.json`); -const englishItemsFile = path.join(process.cwd(), "src/generated", "sidebar.items.json"); -const navItems = JSON.parse(fs.readFileSync(fs.existsSync(localeItemsFile) ? localeItemsFile : englishItemsFile, "utf8")) as ConfigItem[]; +const navItems = loadGeneratedNavigation(localeParam).items; const sidebar = sidebarOn ? buildRailFromConfig(navItems, browserPath, [localeParam]) : false; const sections = sectionsFromConfig(navItems, browserPath); const prevNext = await getPrevNext(browserPath, { @@ -95,7 +112,7 @@ const layoutMode = entry.data.mode === "custom" ? "custom" : "doc"; const data = entry.data as { title?: string; sidebarTitle?: string }; const pageTitle = data.title ?? data.sidebarTitle ?? entry.id.split("/").pop() ?? entry.id; const markdownUrl = Astro.site - ? new URL(withBase(`/${localeParam}/${entry.id}/index.md`), Astro.site).href + ? new URL(withBase(`/${entry.id}.md`), Astro.site).href : undefined; // Untranslated pages point search engines at the English original. const canonicalUrl = fallback && Astro.site ? new URL(withBase(`/${entry.id}/`), Astro.site).href : undefined; @@ -105,7 +122,7 @@ const alternates = Astro.site ? [ { hreflang: "en", href: new URL(withBase(`/${entry.id}/`), Astro.site).href }, { hreflang: "x-default", href: new URL(withBase(`/${entry.id}/`), Astro.site).href }, - ...ACTIVE_LOCALES.map((l) => ({ hreflang: HREFLANG[localeCollectionName(l)] ?? l, href: new URL(withBase(`/${localeRouteName(l)}/${entry.id}/`), Astro.site!).href })), + ...AVAILABLE_LOCALES.map((l) => ({ hreflang: HREFLANG[localeCollectionName(l)] ?? l, href: new URL(withBase(`/${localeRouteName(l)}/${entry.id}/`), Astro.site!).href })), ] : []; const htmlLang = HREFLANG[localeCode] ?? localeParam; @@ -137,7 +154,7 @@ const fallbackNotice = NOT_TRANSLATED[localeCode] ?? { label: "Note", title: "No canonicalUrl={canonicalUrl} sections={sections} currentLocale={localeParam} - activeLocales={ACTIVE_LOCALES} + activeLocales={AVAILABLE_LOCALES} searchable={entry.data.searchable} noindex={entry.data.noindex} markdownUrl={markdownUrl} diff --git a/src/pages/[locale]/[...slug]/index.md.ts b/src/pages/[locale]/[...slug]/index.md.ts deleted file mode 100644 index 693a5a4d2..000000000 --- a/src/pages/[locale]/[...slug]/index.md.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Markdown twins for locale pages: `///index.md`. Untranslated - * (fallback) pages have no twin of their own; the English twin is canonical. - */ -import { cleanMarkdown, entriesFor, type IndexedEntry } from "../../../lib/corpus"; -import { config } from "virtual:nimbus/config"; -import { withBase } from "../../../lib/base"; -import { ACTIVE_LOCALES, localeCollectionName } from "../../../content.config"; -import { localeRouteName } from "../../../util/locales"; -import { prepareAgentMarkdown } from "../../../lib/agent-links"; - -export const prerender = true; - -interface Props { - item: IndexedEntry; -} - -export async function getStaticPaths() { - const paths: Array<{ params: { locale: string; slug: string }; props: Props }> = []; - for (const sourceLocale of ACTIVE_LOCALES) { - const locale = localeRouteName(sourceLocale); - for (const item of await entriesFor(localeCollectionName(sourceLocale))) { - paths.push({ params: { locale, slug: item.entry.id }, props: { item } }); - } - } - return paths; -} - -export async function GET({ props, params }: { props: Props; params: { locale: string } }) { - const { item } = props; - const { entry, title, description } = item; - const markdown = cleanMarkdown(entry); - const body = [ - "---", - `title: ${JSON.stringify(title)}`, - ...(description ? [`description: ${JSON.stringify(description)}`] : []), - `lang: ${JSON.stringify(params.locale)}`, - "---", - "", - "> Documentation Index", - "> A complete documentation index can be fetched from [https://clickhouse.com/docs/llms.txt](https://clickhouse.com/docs/llms.txt)", - "", - `# ${title}`, - "", - markdown, - "", - `Source: ${new URL(withBase(`/${params.locale}/${entry.id}/`), config.site).href}`, - "", - ].join("\n"); - const pagePath = withBase(`/${params.locale}/${entry.id}/`); - return new Response(prepareAgentMarkdown(body, pagePath), { - headers: { "Content-Type": "text/markdown; charset=utf-8" }, - }); -} diff --git a/src/pages/[locale]/[section]/llms-full.txt.ts b/src/pages/[locale]/[section]/llms-full.txt.ts deleted file mode 100644 index e6831bcc0..000000000 --- a/src/pages/[locale]/[section]/llms-full.txt.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Per-locale, per-section corpus: //
/llms-full.txt -import { config } from "virtual:nimbus/config"; -import { entriesFor, renderCorpus, sectionOf, sectionsOf } from "../../../lib/corpus"; -import { ACTIVE_LOCALES, localeCollectionName } from "../../../content.config"; -import { localeRouteName } from "../../../util/locales"; - -export const prerender = true; - -export async function getStaticPaths() { - const paths: Array<{ params: { locale: string; section: string } }> = []; - for (const sourceLocale of ACTIVE_LOCALES) { - const locale = localeRouteName(sourceLocale); - for (const section of sectionsOf(await entriesFor(localeCollectionName(sourceLocale)))) paths.push({ params: { locale, section } }); - } - return paths; -} - -export async function GET({ params }: { params: { locale: string; section: string } }) { - const entries = (await entriesFor(localeCollectionName(params.locale))).filter((i) => sectionOf(i) === params.section); - return new Response(renderCorpus(entries, `${config.title} (${params.locale}) / ${params.section}`, `/${params.locale}/llms.txt`), { - headers: { "Content-Type": "text/plain; charset=utf-8" }, - }); -} diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro index 6c3a36cf6..17ff5c624 100644 --- a/src/pages/[locale]/index.astro +++ b/src/pages/[locale]/index.astro @@ -6,22 +6,18 @@ */ import BaseLayout from "../../layouts/BaseLayout.astro"; import Header from "../../components/Header.astro"; -import HomepageAr from "../../generated/homepage/ar.jsx"; -import HomepageEs from "../../generated/homepage/es.jsx"; -import HomepageFr from "../../generated/homepage/fr.jsx"; -import HomepageJa from "../../generated/homepage/ja.jsx"; -import HomepageKo from "../../generated/homepage/ko.jsx"; -import HomepagePtBr from "../../generated/homepage/pt-br.jsx"; -import HomepageRu from "../../generated/homepage/ru.jsx"; -import HomepageZh from "../../generated/homepage/zh.jsx"; +import ActiveHomepage from "@active-homepage"; import { config } from "virtual:nimbus/config"; -import { ACTIVE_LOCALES, localeCollectionName } from "../../content.config"; +import { ACTIVE_LOCALES, AVAILABLE_LOCALES, localeCollectionName } from "../../content.config"; import { localeInfo, localeRouteName } from "../../util/locales"; export const prerender = true; export function getStaticPaths() { - return ACTIVE_LOCALES.map((locale) => ({ params: { locale: localeRouteName(locale) } })); + return ACTIVE_LOCALES.map((locale) => ({ + params: { locale: localeRouteName(locale) }, + cacheKey: `homepage:${locale}`, + })); } const locale = Astro.params.locale as string; @@ -33,15 +29,8 @@ if (!["ar", "es", "fr", "ja", "ko", "pt-br", "ru", "zh"].includes(localeCode)) { --- -
+
- {localeCode === "ar" && } - {localeCode === "es" && } - {localeCode === "fr" && } - {localeCode === "ja" && } - {localeCode === "ko" && } - {localeCode === "pt-br" && } - {localeCode === "ru" && } - {localeCode === "zh" && } +
diff --git a/src/pages/[locale]/llms-full.txt.ts b/src/pages/[locale]/llms-full.txt.ts deleted file mode 100644 index 59e5f5d44..000000000 --- a/src/pages/[locale]/llms-full.txt.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Per-locale corpus index: //llms-full.txt -import { config } from "virtual:nimbus/config"; -import { entriesFor, renderCorpusIndex, sectionsOf } from "../../lib/corpus"; -import { ACTIVE_LOCALES, localeCollectionName } from "../../content.config"; -import { localeRouteName } from "../../util/locales"; - -export const prerender = true; - -export function getStaticPaths() { - return ACTIVE_LOCALES.map((locale) => ({ params: { locale: localeRouteName(locale) } })); -} - -export async function GET({ params }: { params: { locale: string } }) { - const sections = sectionsOf(await entriesFor(localeCollectionName(params.locale))); - return new Response(renderCorpusIndex(`${config.title} (${params.locale})`, sections, `/${params.locale}`), { - headers: { "Content-Type": "text/plain; charset=utf-8" }, - }); -} diff --git a/src/pages/[locale]/llms.txt.ts b/src/pages/[locale]/llms.txt.ts deleted file mode 100644 index 698df4252..000000000 --- a/src/pages/[locale]/llms.txt.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Per-locale agent index: `//llms.txt` lists the translated pages of - * that locale with links to their markdown twins. - */ -import { entriesFor } from "../../lib/corpus"; -import { config } from "virtual:nimbus/config"; -import { withBase } from "../../lib/base"; -import { ACTIVE_LOCALES, localeCollectionName } from "../../content.config"; -import { localeRouteName } from "../../util/locales"; - -export const prerender = true; - -export function getStaticPaths() { - return ACTIVE_LOCALES.map((locale) => ({ params: { locale: localeRouteName(locale) } })); -} - -export async function GET({ params }: { params: { locale: string } }) { - const locale = params.locale; - const items = await entriesFor(localeCollectionName(locale)); - items.sort((a, b) => a.entry.id.localeCompare(b.entry.id)); - const lines = [ - `# ${config.title} (${locale})`, - "", - config.description ?? "", - "", - `English index: ${new URL(withBase("/llms.txt"), config.site).href}`, - "", - "## Pages", - "", - ...items.map((i) => { - const url = new URL(withBase(`/${locale}/${i.entry.id}/index.md`), config.site).href; - return `- [${i.title}](${url})${i.description ? ` β€” ${i.description}` : ""}`; - }), - "", - ]; - return new Response(lines.join("\n"), { headers: { "Content-Type": "text/plain; charset=utf-8" } }); -} diff --git a/src/pages/[section]/llms-full.txt.ts b/src/pages/[section]/llms-full.txt.ts index 5a66c6fad..60683eeaf 100644 --- a/src/pages/[section]/llms-full.txt.ts +++ b/src/pages/[section]/llms-full.txt.ts @@ -2,11 +2,16 @@ import { config } from "virtual:nimbus/config"; import { englishCorpusEntries, sectionsOf } from "../../lib/corpus"; import { withBase } from "../../lib/base"; +import { EMIT_ENGLISH } from "../../content.config"; export const prerender = true; export async function getStaticPaths() { - return sectionsOf(await englishCorpusEntries()).map((section) => ({ params: { section } })); + if (!EMIT_ENGLISH) return []; + return sectionsOf(await englishCorpusEntries()).map((section) => ({ + params: { section }, + cacheKey: `llms-full-pointer:${section}`, + })); } export async function GET({ params }: { params: { section: string } }) { diff --git a/src/pages/changelog/[product]/[...slug].astro b/src/pages/changelog/[product]/[...slug].astro index 4d73c8c28..06d8b13ab 100644 --- a/src/pages/changelog/[product]/[...slug].astro +++ b/src/pages/changelog/[product]/[...slug].astro @@ -6,12 +6,17 @@ import { readScope } from "../../../lib/scope"; export const prerender = true; export async function getStaticPaths() { - if (readScope().remotePreview) return []; + const scope = readScope(); + if (!scope.emitEnglish || scope.remotePreview) return []; const entries = await getCollection("changelog"); return entries.map((entry) => { const [product, ...slug] = entry.id.split("/"); if (!product || !slug.length) throw new Error(`Invalid changelog id: ${entry.id}`); - return { params: { product, slug: slug.join("/") }, props: { entry } }; + return { + params: { product, slug: slug.join("/") }, + props: { entry }, + cacheKey: entry.digest, + }; }); } diff --git a/src/pages/changelog/[product]/index.astro b/src/pages/changelog/[product]/index.astro index 341ff5514..b4a0387ca 100644 --- a/src/pages/changelog/[product]/index.astro +++ b/src/pages/changelog/[product]/index.astro @@ -4,11 +4,19 @@ import BaseLayout from "../../../layouts/BaseLayout.astro"; import Header from "../../../components/Header.astro"; import { changelogPath, entriesForProduct, productTitle, type ChangelogEntry } from "../../../lib/changelog"; import { readScope } from "../../../lib/scope"; +import { createHash } from "node:crypto"; export async function getStaticPaths() { - if (readScope().remotePreview) return []; + const scope = readScope(); + if (!scope.emitEnglish || scope.remotePreview) return []; const entries = await getCollection("changelog"); - return ["cloud", "oss"].map((product) => ({ params: { product }, props: { entries: entriesForProduct(entries, product) } })); + return ["cloud", "oss"].map((product) => { + const productEntries = entriesForProduct(entries, product); + const cacheKey = createHash("sha256") + .update(productEntries.map((entry) => `${entry.id}:${entry.digest}`).join("\n")) + .digest("hex"); + return { params: { product }, props: { entries: productEntries }, cacheKey }; + }); } const product = Astro.params.product as string; diff --git a/src/pages/changelog/[product]/rss.xml.ts b/src/pages/changelog/[product]/rss.xml.ts index b1d4cd747..1119108da 100644 --- a/src/pages/changelog/[product]/rss.xml.ts +++ b/src/pages/changelog/[product]/rss.xml.ts @@ -1,15 +1,23 @@ import { getCollection } from "astro:content"; import { changelogPath, entriesForProduct, productTitle, type ChangelogEntry } from "../../../lib/changelog"; import { readScope } from "../../../lib/scope"; +import { createHash } from "node:crypto"; function xml(value: string): string { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); } export async function getStaticPaths() { - if (readScope().remotePreview) return []; + const scope = readScope(); + if (!scope.emitEnglish || scope.remotePreview) return []; const entries = await getCollection("changelog"); - return ["cloud", "oss"].map((product) => ({ params: { product }, props: { entries: entriesForProduct(entries, product) } })); + return ["cloud", "oss"].map((product) => { + const productEntries = entriesForProduct(entries, product); + const cacheKey = createHash("sha256") + .update(productEntries.map((entry) => `${entry.id}:${entry.digest}`).join("\n")) + .digest("hex"); + return { params: { product }, props: { entries: productEntries }, cacheKey }; + }); } export function GET({ params, props }: { params: { product: string }; props: { entries: ChangelogEntry[] } }) { diff --git a/src/pages/clickstack/api-reference/[tag]/[operation].astro b/src/pages/clickstack/api-reference/[tag]/[operation].astro index 8fa9525ad..4c5c20265 100644 --- a/src/pages/clickstack/api-reference/[tag]/[operation].astro +++ b/src/pages/clickstack/api-reference/[tag]/[operation].astro @@ -1,14 +1,16 @@ --- import ApiPage from "@/components/api/ApiPage.astro"; -import { getApiOperations, getRoutedApiOperation } from "@/lib/openapi"; +import { apiPageCacheKey, getApiOperations, getRoutedApiOperation } from "@/lib/openapi"; import { readScope } from "@/lib/scope"; export const prerender = true; export function getStaticPaths() { - if (readScope().remotePreview) return []; + const scope = readScope(); + if (!scope.emitEnglish || scope.remotePreview) return []; return getApiOperations("clickstack").map((page) => ({ params: { tag: page.tagSlug, operation: page.slug }, + cacheKey: apiPageCacheKey(page), })); } diff --git a/src/pages/clickstack/api-reference/[tag]/[operation]/index.md.ts b/src/pages/clickstack/api-reference/[tag]/[operation].md.ts similarity index 60% rename from src/pages/clickstack/api-reference/[tag]/[operation]/index.md.ts rename to src/pages/clickstack/api-reference/[tag]/[operation].md.ts index f067c3ad1..c6376c5ed 100644 --- a/src/pages/clickstack/api-reference/[tag]/[operation]/index.md.ts +++ b/src/pages/clickstack/api-reference/[tag]/[operation].md.ts @@ -1,13 +1,17 @@ import type { APIRoute } from "astro"; -import { getApiOperation, getApiOperations, renderApiOperationMarkdown } from "@/lib/openapi"; +import { apiPageCacheKey, getApiOperation, getApiOperations, renderApiOperationMarkdown } from "@/lib/openapi"; import { readScope } from "@/lib/scope"; import { prepareAgentMarkdown } from "@/lib/agent-links"; export const prerender = true; export function getStaticPaths() { - if (readScope().remotePreview) return []; - return getApiOperations("clickstack").map((page) => ({ params: { tag: page.tagSlug, operation: page.slug } })); + const scope = readScope(); + if (!scope.emitEnglish || scope.remotePreview) return []; + return getApiOperations("clickstack").map((page) => ({ + params: { tag: page.tagSlug, operation: page.slug }, + cacheKey: apiPageCacheKey(page), + })); } export const GET: APIRoute = async ({ params }) => { diff --git a/src/pages/clickstack/api-reference/[tag]/[operation]/index.mdx.ts b/src/pages/clickstack/api-reference/[tag]/[operation]/index.mdx.ts deleted file mode 100644 index 0bf8ef9f8..000000000 --- a/src/pages/clickstack/api-reference/[tag]/[operation]/index.mdx.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { APIRoute } from "astro"; -import { getApiOperation, getApiOperations, renderApiOperationMarkdown } from "@/lib/openapi"; -import { readScope } from "@/lib/scope"; - -export const prerender = true; - -export function getStaticPaths() { - if (readScope().remotePreview) return []; - return getApiOperations("clickstack").map((page) => ({ params: { tag: page.tagSlug, operation: page.slug } })); -} - -export const GET: APIRoute = async ({ params }) => new Response( - await renderApiOperationMarkdown(getApiOperation("clickstack", params.tag!, params.operation!)), - { headers: { "Content-Type": "text/markdown; charset=utf-8" } }, -); diff --git a/src/pages/index.astro b/src/pages/index.astro index 99f131e33..ff7a7037a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,9 +3,10 @@ import BaseLayout from "../layouts/BaseLayout.astro"; import Header from "../components/Header.astro"; import Homepage from "../generated/homepage/en.jsx"; import { config } from "virtual:nimbus/config"; +import { AVAILABLE_LOCALES } from "../content.config"; --- -
+
diff --git a/src/pages/nav/[...key].astro b/src/pages/nav/[...key].astro index 01259de8a..53dbbe2e5 100644 --- a/src/pages/nav/[...key].astro +++ b/src/pages/nav/[...key].astro @@ -5,33 +5,42 @@ * a collapsed group is first expanded. Built from src/generated/sidebar.items.json. */ import SidebarChildren from "@/components/ui/sidebar/SidebarChildren.astro"; -import { collectGroups, toRendered, type ConfigItem } from "@/lib/sidebar-lazy"; +import { LAZY_MIN_CHILDREN, collectGroups, toRendered, type ConfigItem } from "@/lib/sidebar-lazy"; // Generated by bin/gen-sidebar.ts in `prebuild`; a static import keeps the // route independent of the build's working directory. import sidebarItems from "@/generated/sidebar.items.json"; import fs from "node:fs"; import nodePath from "node:path"; -import { ACTIVE_LOCALES } from "@/content.config"; +import { AVAILABLE_LOCALES, EMIT_ENGLISH } from "@/content.config"; import { localeRouteName } from "@/util/locales"; import type { SidebarItem } from "@cloudflare/nimbus-docs/types"; +import { createHash } from "node:crypto"; export const partial = true; export const prerender = true; export function getStaticPaths() { - const paths: Array<{ params: { key: string }; props: { items: SidebarItem[] } }> = []; - const trees: Array<{ prefix: string[]; items: ConfigItem[] }> = [{ prefix: [], items: sidebarItems as ConfigItem[] }]; + const paths: Array<{ params: { key: string }; props: { items: SidebarItem[] }; cacheKey: string }> = []; + const trees: Array<{ prefix: string[]; items: ConfigItem[] }> = EMIT_ENGLISH + ? [{ prefix: [], items: sidebarItems as ConfigItem[] }] + : []; // Locale trees (generated by bin/gen-sidebar.ts --locale) are namespaced under /nav//. - for (const locale of ACTIVE_LOCALES) { + for (const locale of EMIT_ENGLISH ? AVAILABLE_LOCALES : []) { const file = nodePath.join(process.cwd(), "src/generated", `sidebar.items.${locale}.json`); if (fs.existsSync(file)) trees.push({ prefix: [localeRouteName(locale)], items: JSON.parse(fs.readFileSync(file, "utf8")) as ConfigItem[] }); } const seen = new Set(); for (const tree of trees) { for (const g of collectGroups(tree.items, tree.prefix)) { + if (g.items.length < LAZY_MIN_CHILDREN) continue; if (seen.has(g.key)) throw new Error(`sidebar-lazy: duplicate group key "${g.key}" (${g.label})`); seen.add(g.key); - paths.push({ params: { key: g.key }, props: { items: toRendered(g.items, g.path) } }); + const items = toRendered(g.items, g.path); + paths.push({ + params: { key: g.key }, + props: { items }, + cacheKey: createHash("sha256").update(JSON.stringify(items)).digest("hex"), + }); } } return paths; diff --git a/src/pages/products/cloud/api-reference/[tag]/[operation].astro b/src/pages/products/cloud/api-reference/[tag]/[operation].astro index 230c818a1..7a8f67f04 100644 --- a/src/pages/products/cloud/api-reference/[tag]/[operation].astro +++ b/src/pages/products/cloud/api-reference/[tag]/[operation].astro @@ -1,14 +1,16 @@ --- import ApiPage from "@/components/api/ApiPage.astro"; -import { getApiOperations, getRoutedApiOperation } from "@/lib/openapi"; +import { apiPageCacheKey, getApiOperations, getRoutedApiOperation } from "@/lib/openapi"; import { readScope } from "@/lib/scope"; export const prerender = true; export function getStaticPaths() { - if (readScope().remotePreview) return []; + const scope = readScope(); + if (!scope.emitEnglish || scope.remotePreview) return []; return getApiOperations("cloud").map((page) => ({ params: { tag: page.tagSlug, operation: page.slug }, + cacheKey: apiPageCacheKey(page), })); } diff --git a/src/pages/products/cloud/api-reference/[tag]/[operation]/index.md.ts b/src/pages/products/cloud/api-reference/[tag]/[operation].md.ts similarity index 60% rename from src/pages/products/cloud/api-reference/[tag]/[operation]/index.md.ts rename to src/pages/products/cloud/api-reference/[tag]/[operation].md.ts index 29469aff8..e21e36744 100644 --- a/src/pages/products/cloud/api-reference/[tag]/[operation]/index.md.ts +++ b/src/pages/products/cloud/api-reference/[tag]/[operation].md.ts @@ -1,13 +1,17 @@ import type { APIRoute } from "astro"; -import { getApiOperation, getApiOperations, renderApiOperationMarkdown } from "@/lib/openapi"; +import { apiPageCacheKey, getApiOperation, getApiOperations, renderApiOperationMarkdown } from "@/lib/openapi"; import { readScope } from "@/lib/scope"; import { prepareAgentMarkdown } from "@/lib/agent-links"; export const prerender = true; export function getStaticPaths() { - if (readScope().remotePreview) return []; - return getApiOperations("cloud").map((page) => ({ params: { tag: page.tagSlug, operation: page.slug } })); + const scope = readScope(); + if (!scope.emitEnglish || scope.remotePreview) return []; + return getApiOperations("cloud").map((page) => ({ + params: { tag: page.tagSlug, operation: page.slug }, + cacheKey: apiPageCacheKey(page), + })); } export const GET: APIRoute = async ({ params }) => { diff --git a/src/pages/products/cloud/api-reference/[tag]/[operation]/index.mdx.ts b/src/pages/products/cloud/api-reference/[tag]/[operation]/index.mdx.ts deleted file mode 100644 index 3693cc671..000000000 --- a/src/pages/products/cloud/api-reference/[tag]/[operation]/index.mdx.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { APIRoute } from "astro"; -import { getApiOperation, getApiOperations, renderApiOperationMarkdown } from "@/lib/openapi"; -import { readScope } from "@/lib/scope"; - -export const prerender = true; - -export function getStaticPaths() { - if (readScope().remotePreview) return []; - return getApiOperations("cloud").map((page) => ({ params: { tag: page.tagSlug, operation: page.slug } })); -} - -export const GET: APIRoute = async ({ params }) => new Response( - await renderApiOperationMarkdown(getApiOperation("cloud", params.tag!, params.operation!)), - { headers: { "Content-Type": "text/markdown; charset=utf-8" } }, -); diff --git a/tsconfig.json b/tsconfig.json index eb886e904..8a75640eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "jsxImportSource": "react", "paths": { "@/*": ["src/*"], - "~/*": ["src/*"] + "~/*": ["src/*"], + "@active-homepage": ["src/generated/homepage/en.jsx"] } }, "include": [".astro/types.d.ts", "src/**/*", "bin/**/*", "astro.config.ts"], diff --git a/vercel.json b/vercel.json index 4064f1a6b..b9e03b848 100644 --- a/vercel.json +++ b/vercel.json @@ -11,6 +11,41 @@ "source": "/", "destination": "/docs", "permanent": false + }, + { + "source": "/docs/:locale(ar|es|fr|ja|ko|pt-BR|ru|zh)/llms.txt", + "destination": "/docs/llms.txt", + "permanent": true + }, + { + "source": "/docs/:locale(ar|es|fr|ja|ko|pt-BR|ru|zh)/llms-full.txt", + "destination": "/docs/llms-full.txt", + "permanent": true + }, + { + "source": "/docs/:locale(ar|es|fr|ja|ko|pt-BR|ru|zh)/:section/llms-full.txt", + "destination": "/docs/:section/llms-full.txt", + "permanent": true + }, + { + "source": "/docs/:locale(ar|es|fr|ja|ko|pt-BR|ru|zh)/index.md", + "destination": "/docs/index.md", + "permanent": true + }, + { + "source": "/docs/:locale(ar|es|fr|ja|ko|pt-BR|ru|zh)/:path+/index.md", + "destination": "/docs/:path+.md", + "permanent": true + }, + { + "source": "/docs/:locale(ar|es|fr|ja|ko|pt-BR|ru|zh)/:path+.md", + "destination": "/docs/:path+.md", + "permanent": true + }, + { + "source": "/docs/:path+/index.md", + "destination": "/docs/:path+.md", + "permanent": true } ], "rewrites": [ From 91ea1215f2784fb99103bbd83efdb6d7397b7b7b Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:47:48 +0200 Subject: [PATCH 3/5] Improve documentation preview status comment --- .github/workflows/site-preview.yml | 63 +++++++++++++++++------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/.github/workflows/site-preview.yml b/.github/workflows/site-preview.yml index ccb4e3990..3231073f5 100644 --- a/.github/workflows/site-preview.yml +++ b/.github/workflows/site-preview.yml @@ -150,6 +150,9 @@ jobs: HEAD_SHA: ${{ steps.pull-request.outputs.head_sha }} PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || inputs.pull_request_number }} TRANSLATION_SCOPE: ${{ steps.translations.outputs.scope }} + TRANSLATION_SUMMARY: ${{ steps.translations.outputs.summary }} + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} TRUST: ${{ steps.pull-request.outputs.trust }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} @@ -158,6 +161,29 @@ jobs: repository_owner="${GITHUB_REPOSITORY%%/*}" repository_name="${GITHUB_REPOSITORY#*/}" + update_preview_comment() { + deployment_status="$1" + updated_at="$(date -u '+%Y-%m-%d %H:%M UTC')" + marker="" + docs_url="${preview_url%/}/docs" + body="$(printf 'πŸ•΅ %s\n\n| Preview | Deployment | Components | Updated (UTC) |\n| --- | --- | --- | --- |\n| [Docs preview](%s) | [%s](%s) | %s | %s |' \ + "$marker" \ + "$docs_url" \ + "$deployment_status" \ + "$preview_url" \ + "$TRANSLATION_SUMMARY" \ + "$updated_at")" + comment_id="$({ + gh api --paginate "repos/$GH_REPO/issues/$PULL_REQUEST_NUMBER/comments" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"$marker\")) | .id" + } | tail -n 1)" + if [[ -n "$comment_id" ]]; then + gh api --method PATCH "repos/$GH_REPO/issues/comments/$comment_id" -f body="$body" >/dev/null + else + gh pr comment "$PULL_REQUEST_NUMBER" --body "$body" + fi + } + if ! project_response="$( curl --silent --show-error --fail-with-body \ --header "Authorization: Bearer $VERCEL_TOKEN" \ @@ -225,6 +251,11 @@ jobs: preview_url="https://${deployment_url#https://}" echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT" echo "Vercel deployment: $preview_url" + if [[ ! "$preview_url" =~ ^https://[^[:space:]]+$ ]]; then + echo "Vercel returned an invalid preview URL." >&2 + exit 1 + fi + update_preview_comment "🟠 Building" for _ in {1..360}; do if ! deployment_response="$( @@ -232,6 +263,7 @@ jobs: --header "Authorization: Bearer $VERCEL_TOKEN" \ "https://api.vercel.com/v13/deployments/$deployment_id?teamId=$VERCEL_ORG_ID" )"; then + update_preview_comment "πŸ”΄ Failed" printf '%s\n' "$deployment_response" >&2 exit 1 fi @@ -242,6 +274,7 @@ jobs: break ;; ERROR|CANCELED|DELETED|BLOCKED) + update_preview_comment "πŸ”΄ Failed" jq -r '(.errorCode // "deployment_failed") + ": " + (.errorMessage // "Vercel deployment did not complete")' \ <<< "$deployment_response" >&2 exit 1 @@ -250,6 +283,7 @@ jobs: sleep 10 ;; *) + update_preview_comment "πŸ”΄ Failed" echo "Unexpected Vercel deployment state: $ready_state" >&2 exit 1 ;; @@ -257,33 +291,8 @@ jobs: done if [[ "$ready_state" != "READY" ]]; then + update_preview_comment "πŸ”΄ Failed" echo "Timed out waiting for Vercel deployment $deployment_id." >&2 exit 1 fi - if [[ ! "$preview_url" =~ ^https://[^[:space:]]+$ ]]; then - echo "Vercel returned an invalid preview URL." >&2 - exit 1 - fi - - - name: Add the preview link to the pull request - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} - PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || inputs.pull_request_number }} - TRANSLATION_SUMMARY: ${{ steps.translations.outputs.summary }} - TRUST: ${{ steps.pull-request.outputs.trust }} - run: | - marker="" - body="$(printf '%s\n%s' \ - "$marker" \ - "Nimbus documentation preview ($TRUST, $TRANSLATION_SUMMARY): $PREVIEW_URL")" - comment_id="$({ - gh api --paginate "repos/$GH_REPO/issues/$PULL_REQUEST_NUMBER/comments" \ - --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"$marker\")) | .id" - } | tail -n 1)" - if [[ -n "$comment_id" ]]; then - gh api --method PATCH "repos/$GH_REPO/issues/comments/$comment_id" -f body="$body" >/dev/null - else - gh pr comment "$PULL_REQUEST_NUMBER" --body "$body" - fi + update_preview_comment "🟒 Ready" From 10f3e7b7a19db471afcc9c13b7a6306c2976dbed Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:51:24 +0200 Subject: [PATCH 4/5] Rename documentation preview workflow --- .github/workflows/site-preview.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/site-preview.yml b/.github/workflows/site-preview.yml index 3231073f5..8403301c9 100644 --- a/.github/workflows/site-preview.yml +++ b/.github/workflows/site-preview.yml @@ -1,6 +1,6 @@ -name: Deploy Nimbus site preview +name: Deploy docs preview -run-name: Preview Nimbus PR #${{ github.event.pull_request.number || inputs.pull_request_number }} +run-name: Deploy docs preview for PR #${{ github.event.pull_request.number || inputs.pull_request_number }} on: pull_request_target: @@ -255,7 +255,7 @@ jobs: echo "Vercel returned an invalid preview URL." >&2 exit 1 fi - update_preview_comment "🟠 Building" + update_preview_comment "🟑 Building" for _ in {1..360}; do if ! deployment_response="$( From 3d4b5f5cde86145b5ec60578b2413a449c013eea Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:04:36 +0200 Subject: [PATCH 5/5] Address preview workflow review comments --- .github/workflows/site-preview.yml | 92 +++++++++++++++++++++++++----- clickstack/architecture.mdx | 2 - 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/.github/workflows/site-preview.yml b/.github/workflows/site-preview.yml index 8403301c9..47b52ad83 100644 --- a/.github/workflows/site-preview.yml +++ b/.github/workflows/site-preview.yml @@ -30,6 +30,35 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: + # A superseding run may start after GitHub terminated the previous runner + # before its EXIT trap completed. Clear that stale state before any + # validation in this run can fail. + - name: Clear an interrupted preview status + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || inputs.pull_request_number }} + run: | + marker="" + comment_id="$({ + gh api --paginate "repos/$GH_REPO/issues/$PULL_REQUEST_NUMBER/comments" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"$marker\")) | .id" + } | tail -n 1)" + if [[ -z "$comment_id" ]]; then + exit 0 + fi + + body="$(gh api "repos/$GH_REPO/issues/comments/$comment_id" --jq .body)" + if [[ "$body" != *"🟑 Building"* ]]; then + exit 0 + fi + + updated_at="$(date -u '+%Y-%m-%d %H:%M UTC')" + body="$(jq -nr --arg body "$body" --arg updated_at "$updated_at" \ + '$body | gsub("🟑 Building"; "πŸ”΄ Failed") | sub("\\| [^|\\n]+ \\|$"; "| \($updated_at) |")')" + gh api --method PATCH "repos/$GH_REPO/issues/comments/$comment_id" -f body="$body" >/dev/null + - name: Require a trusted manual invocation if: github.event_name == 'workflow_dispatch' env: @@ -162,10 +191,12 @@ jobs: repository_name="${GITHUB_REPOSITORY#*/}" update_preview_comment() { - deployment_status="$1" - updated_at="$(date -u '+%Y-%m-%d %H:%M UTC')" - marker="" - docs_url="${preview_url%/}/docs" + local deployment_status="$1" + local updated_at="$(date -u '+%Y-%m-%d %H:%M UTC')" + local marker="" + local docs_url="${preview_url%/}/docs" + local body + local comment_id body="$(printf 'πŸ•΅ %s\n\n| Preview | Deployment | Components | Updated (UTC) |\n| --- | --- | --- | --- |\n| [Docs preview](%s) | [%s](%s) | %s | %s |' \ "$marker" \ "$docs_url" \ @@ -173,10 +204,12 @@ jobs: "$preview_url" \ "$TRANSLATION_SUMMARY" \ "$updated_at")" - comment_id="$({ + if ! comment_id="$({ gh api --paginate "repos/$GH_REPO/issues/$PULL_REQUEST_NUMBER/comments" \ --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"$marker\")) | .id" - } | tail -n 1)" + } | tail -n 1)"; then + return 1 + fi if [[ -n "$comment_id" ]]; then gh api --method PATCH "repos/$GH_REPO/issues/comments/$comment_id" -f body="$body" >/dev/null else @@ -184,6 +217,33 @@ jobs: fi } + update_preview_comment_with_retry() { + local deployment_status="$1" + local attempt + for attempt in 1 2 3; do + if update_preview_comment "$deployment_status"; then + return 0 + fi + echo "::warning::Unable to update the preview comment (attempt $attempt of 3)." + if (( attempt < 3 )); then + sleep 2 + fi + done + return 1 + } + + preview_finished=false + finalize_preview_comment() { + local exit_code=$? + trap - EXIT + if [[ "$preview_finished" != true ]]; then + if ! update_preview_comment_with_retry "πŸ”΄ Failed"; then + echo "::warning::Unable to mark the preview comment as failed." + fi + fi + exit "$exit_code" + } + if ! project_response="$( curl --silent --show-error --fail-with-body \ --header "Authorization: Bearer $VERCEL_TOKEN" \ @@ -255,15 +315,21 @@ jobs: echo "Vercel returned an invalid preview URL." >&2 exit 1 fi - update_preview_comment "🟑 Building" + trap finalize_preview_comment EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + if ! update_preview_comment_with_retry "🟑 Building"; then + echo "::warning::Unable to mark the preview comment as building." + fi - for _ in {1..360}; do + # Leave five minutes for the EXIT trap to update the comment before + # the 60-minute job timeout terminates the runner. + for _ in {1..330}; do if ! deployment_response="$( curl --silent --show-error --fail-with-body \ --header "Authorization: Bearer $VERCEL_TOKEN" \ "https://api.vercel.com/v13/deployments/$deployment_id?teamId=$VERCEL_ORG_ID" )"; then - update_preview_comment "πŸ”΄ Failed" printf '%s\n' "$deployment_response" >&2 exit 1 fi @@ -274,7 +340,6 @@ jobs: break ;; ERROR|CANCELED|DELETED|BLOCKED) - update_preview_comment "πŸ”΄ Failed" jq -r '(.errorCode // "deployment_failed") + ": " + (.errorMessage // "Vercel deployment did not complete")' \ <<< "$deployment_response" >&2 exit 1 @@ -283,7 +348,6 @@ jobs: sleep 10 ;; *) - update_preview_comment "πŸ”΄ Failed" echo "Unexpected Vercel deployment state: $ready_state" >&2 exit 1 ;; @@ -291,8 +355,10 @@ jobs: done if [[ "$ready_state" != "READY" ]]; then - update_preview_comment "πŸ”΄ Failed" echo "Timed out waiting for Vercel deployment $deployment_id." >&2 exit 1 fi - update_preview_comment "🟒 Ready" + if ! update_preview_comment_with_retry "🟒 Ready"; then + echo "::warning::Unable to mark the preview comment as ready." + fi + preview_finished=true diff --git a/clickstack/architecture.mdx b/clickstack/architecture.mdx index ae002f5c7..b0eaa07b7 100644 --- a/clickstack/architecture.mdx +++ b/clickstack/architecture.mdx @@ -9,8 +9,6 @@ keywords: ['ClickStack architecture', 'observability architecture', 'HyperDX', ' import { Image } from "/snippets/components/Image.jsx"; -Test change - The ClickStack architecture varies depending on how it is deployed. There are important architectural distinctions between **ClickStack Open Source**, where all components are self-managed, and **Managed ClickStack**, where ClickHouse and the HyperDX UI are hosted and operated in ClickHouse Cloud. While the core components remain the same in both models, the responsibility for hosting, scaling, and securing each component differs. ## Architecture overview {#architecture-overview}