From f6d1a8255dd7e23c7c9dd0cfa4832adb5d610761 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:33:13 +0000 Subject: [PATCH 1/2] chore(website): serve build-accurate archives of past releases at /v// MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Readers can browse the website exactly as it shipped for any past release at varar.dev/v// — the whole site, interactive editor included, running that release's own code, not just frozen prose. - astro.config.mjs honours VARAR_SITE_BASE so a release can be built under a path prefix; archived versions are marked noindex and link home via a footer "Version history" entry to the /v/ hub. - release/website-*.{sh,mjs} build a version snapshot, rebase hand-written root-absolute links under the prefix, and generate the /v/ archive hub. - website.yml owns website deploy: it snapshots each release tag onto an orphan website-snapshots branch and overlays the archive into the Cloudflare deploy. Backfill past releases via its workflow_dispatch. Deploy moved out of typescript.yml (its path filter can't react to tags); the PR build gate stays. - doc/website-versioning.md documents the mechanism and backfill. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014FaBNvwetMrvHtc5PqVRfa --- .github/workflows/typescript.yml | 43 +--- .github/workflows/website.yml | 193 ++++++++++++++++++ CLAUDE.md | 2 +- doc/website-versioning.md | 77 +++++++ release/website-ensure-base.mjs | 45 ++++ release/website-index.mjs | 160 +++++++++++++++ release/website-rebase-links.mjs | 54 +++++ release/website-snapshot.sh | 52 +++++ typescript/packages/website/astro.config.mjs | 15 ++ .../website/src/components/Footer.astro | 34 +++ typescript/pnpm-workspace.yaml | 2 +- 11 files changed, 636 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/website.yml create mode 100644 doc/website-versioning.md create mode 100755 release/website-ensure-base.mjs create mode 100755 release/website-index.mjs create mode 100755 release/website-rebase-links.mjs create mode 100755 release/website-snapshot.sh create mode 100644 typescript/packages/website/src/components/Footer.astro diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index dd3c4c6a..552774ea 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -51,43 +51,8 @@ jobs: # its components assert at build time that every port in # languages.json has example files, so this is where a forgotten port's # missing examples become a red build instead of surfacing only at deploy. + # + # Deployment (and the versioned archive under /v//) lives in + # website.yml — it needs the release tags this workflow's path filter + # doesn't react to. - run: pnpm --filter @varar/website... build - - deploy-website: - needs: test - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - concurrency: - group: deploy-website - cancel-in-progress: false - defaults: - run: - working-directory: typescript - steps: - - uses: actions/checkout@v7 - - # No version here — pnpm/action-setup reads "packageManager" from - # typescript/package.json, the same field corepack uses locally. - - uses: pnpm/action-setup@v6 - with: - package_json_file: typescript/package.json - - # No version here — .node-version is the pin, read by setup-node and by - # nvm/fnm/asdf alike, so CI and a developer machine cannot drift. - - uses: actions/setup-node@v7 - with: - node-version-file: typescript/.node-version - cache: pnpm - cache-dependency-path: typescript/pnpm-lock.yaml - - - run: pnpm install --frozen-lockfile - - - run: pnpm --filter @varar/website... build - - - uses: cloudflare/wrangler-action@v4 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - workingDirectory: typescript/packages/website - wranglerVersion: "4" - packageManager: pnpm diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 00000000..3a80d174 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,193 @@ +name: Website + +# Builds and deploys https://varar.dev, and maintains the version archive served +# under https://varar.dev/v// — a build-accurate copy of the website as +# it shipped for each past release (interactive editor and all). +# +# Flow: +# - push to main → deploy (live site + existing archives overlaid) +# - push tag v* → snapshot that release, commit it to the orphan +# `website-snapshots` branch, then deploy +# - workflow_dispatch → backfill: snapshot one version (input) or every +# release tag still missing an archive, then deploy +# +# `deploy` chains after `snapshot` via `needs`, so a freshly-archived release +# ships in the same run — no reliance on a bot push re-triggering CI (it doesn't). +# +# The website PR build gate lives in typescript.yml's `test` job and stays there. + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + inputs: + version: + description: 'Backfill: bare semver to (re)snapshot, e.g. 0.6.1. Blank = every release tag missing an archive.' + required: false + default: '' + +jobs: + snapshot: + # Only tag pushes and manual backfills produce archives; a plain main push + # skips straight to deploy. + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: website-snapshots + cancel-in-progress: false + steps: + # Full history + tags: we build each release from a worktree of its tag, + # using the snapshot tooling from this (current) checkout. + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v6 + with: + package_json_file: typescript/package.json + + - uses: actions/setup-node@v7 + with: + node-version-file: typescript/.node-version + cache: pnpm + cache-dependency-path: typescript/pnpm-lock.yaml + + # The archive lives on an orphan `website-snapshots` branch — built assets + # for every past release, kept off main's history. Check it out beside the + # source; start it empty the first time. + - uses: actions/checkout@v7 + id: archive + continue-on-error: true + with: + ref: website-snapshots + path: snapshots + fetch-depth: 0 + + - name: Initialise archive branch if absent + if: steps.archive.outcome != 'success' + run: | + rm -rf snapshots && mkdir snapshots + cd snapshots + git init -q -b website-snapshots + + - name: Compute versions to snapshot + id: versions + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = workflow_dispatch ]; then + if [ -n "${{ inputs.version }}" ]; then + list="${{ inputs.version }}" + else + list="" + for t in $(git tag -l 'v*' | sed 's/^v//' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'); do + [ -d "snapshots/v/$t" ] || list="$list $t" + done + fi + else + list="${GITHUB_REF_NAME#v}" # tag push + fi + echo "list=$(echo "$list" | xargs)" >> "$GITHUB_OUTPUT" + + - name: Build snapshots + if: steps.versions.outputs.list != '' + run: | + set -euo pipefail + mkdir -p snapshots/v + for v in ${{ steps.versions.outputs.list }}; do + echo "::group::snapshot v$v" + wt="$RUNNER_TEMP/src-$v" + rm -rf "$wt" + git worktree add --force "$wt" "v$v" + (cd "$wt/typescript" && pnpm install --frozen-lockfile) + release/website-snapshot.sh "$v" "$PWD/snapshots/v/$v" "$wt" + git worktree remove --force "$wt" + echo "::endgroup::" + done + + - name: Regenerate archive index + run: node release/website-index.mjs snapshots/v + + - name: Commit and push archive + run: | + set -euo pipefail + cd snapshots + git add -A + if git diff --cached --quiet; then + echo "archive unchanged" + exit 0 + fi + git -c user.name='github-actions[bot]' \ + -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ + commit -m "chore(website): archive snapshot(s) ${{ steps.versions.outputs.list }}" + # Explicit token auth so the push works both when this branch was + # checked out (auth in a sibling dir) and when we just init'd it. A + # GITHUB_TOKEN push doesn't re-trigger CI, which is fine: `deploy` + # chains off this job via `needs`, it isn't waiting on a push event. + git remote remove origin 2>/dev/null || true + git remote add origin \ + "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" + git push -u origin website-snapshots + + deploy: + needs: [snapshot] + # Runs after snapshot on tag/dispatch, and on its own for a plain main push + # (where snapshot is skipped). Never on PRs. + if: | + always() && + github.event_name != 'pull_request' && + (needs.snapshot.result == 'success' || needs.snapshot.result == 'skipped') + runs-on: ubuntu-latest + concurrency: + group: deploy-website + cancel-in-progress: false + defaults: + run: + working-directory: typescript + steps: + # Always deploy main's live site, whatever ref triggered the run. + - uses: actions/checkout@v7 + with: + ref: main + + # The version archives to overlay onto the live build. + - uses: actions/checkout@v7 + id: archive + continue-on-error: true + with: + ref: website-snapshots + path: snapshots + + - uses: pnpm/action-setup@v6 + with: + package_json_file: typescript/package.json + + - uses: actions/setup-node@v7 + with: + node-version-file: typescript/.node-version + cache: pnpm + cache-dependency-path: typescript/pnpm-lock.yaml + + - run: pnpm install --frozen-lockfile + + - run: pnpm --filter @varar/website... build + + # Fold the archived versions into the deploy so /v// is served + # from the same Worker as the live site. + - name: Overlay version archives + if: steps.archive.outcome == 'success' + run: | + if [ -d ../snapshots/v ]; then + mkdir -p packages/website/dist/v + cp -R ../snapshots/v/. packages/website/dist/v/ + fi + + - uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: typescript/packages/website + wranglerVersion: "4" + packageManager: pnpm diff --git a/CLAUDE.md b/CLAUDE.md index 945026e3..fb64ed24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ pnpm workspace · biome · vitest (for the core's own tests) · knip · jscpd · `.github/workflows/` (`typescript.yml`, `python.yml`, `java.yml`, `ruby.yml`, `rust.yml`, `dotnet.yml`, `go.yml` — all also trigger on `conformance/**`). - **Trunk-based development.** We commit small, working increments straight to `main` — no long-lived feature branches. Keep each commit self-contained and green (build + tests pass), so trunk is always releasable. -- **Type-check is a separate gate.** vitest runs source through esbuild/tsx, which strips types without checking them — a fully green suite can still fail `tsc`. Run `pnpm -r build` (exit 0) before calling any change done, especially after touching a shared type, an AST node, or a package's public exports (new required fields and new exports are the usual culprits). Note `pnpm build` and `pnpm check` both exclude the website packages — the Starlight website is built separately via `pnpm --filter @varar/website... build`, run in two CI places: the `test` job (a PR gate — its `` components assert every port in `languages.json` has example files, so a forgotten port fails the build) and the `deploy-website` job (which also deploys to https://varar.dev). The legacy `packages/website` is never built. To check the website locally: `pnpm --filter @varar/website build`. +- **Type-check is a separate gate.** vitest runs source through esbuild/tsx, which strips types without checking them — a fully green suite can still fail `tsc`. Run `pnpm -r build` (exit 0) before calling any change done, especially after touching a shared type, an AST node, or a package's public exports (new required fields and new exports are the usual culprits). Note `pnpm build` and `pnpm check` both exclude the website packages — the Starlight website is built separately via `pnpm --filter @varar/website... build`, run in two CI places: the `test` job in `typescript.yml` (a PR gate — its `` components assert every port in `languages.json` has example files, so a forgotten port fails the build) and the `deploy` job in `website.yml` (which also deploys to https://varar.dev, and overlays the versioned archive served under `/v//` — see `doc/website-versioning.md`). The legacy `packages/website` is never built. To check the website locally: `pnpm --filter @varar/website build`. - `pnpm -r build` only type-checks each package's `src/` (its `tsconfig.json` emits with `rootDir: src`). **Test files (`tests/**`) are type-checked by `pnpm typecheck`** (root `tsconfig.tests.json`, `noEmit`, covers every non-website package's `tests/`). It's part of `pnpm check`, so run `pnpm check` (or `pnpm typecheck` alone) after touching tests — a green vitest run does *not* mean the tests type-check. Note `expectTypeOf` assertions are validated here by `tsc`, not by vitest (we don't run `vitest --typecheck`). - **Dogfood specs**: `examples/typescript-vitest` (package `@varar/example-typescript-vitest`, a pnpm workspace member via diff --git a/doc/website-versioning.md b/doc/website-versioning.md new file mode 100644 index 00000000..3867a3a6 --- /dev/null +++ b/doc/website-versioning.md @@ -0,0 +1,77 @@ +# Website version history + +Readers can browse the website as it shipped for any past release at +`https://varar.dev/v//` — for example +[`https://varar.dev/v/0.7.0/`](https://varar.dev/v/0.7.0/). Each archived version +is **build-accurate**: not just the docs prose but the whole site, including the +interactive `` components running exactly the `@varar/varar` code that +shipped in that release. A "Version history" link in the site footer, and the +hub page at [`/v/`](https://varar.dev/v/), list every archived version. + +## Why build-accurate, not content-only + +There is a Starlight plugin (`starlight-versions`) that snapshots docs *content* +into path-prefixed folders. We deliberately don't use it: it freezes Markdown +only, so an archived page's live editor would still run today's core. For a tool +whose whole pitch is "the prose IS the executable spec," that would be a lie — +the v0.5 page would show v0.5 words but v0.9 behaviour. Instead we build each +release tag from its own source and serve the whole thing under a path prefix. + +## How it fits together + +Everything is one Cloudflare Worker (`packages/website/wrangler.jsonc`) serving +static assets, so a versioned build has to live *inside* the deployed `dist/`. +The pieces: + +- **`packages/website/astro.config.mjs`** reads `VARAR_SITE_BASE`. When set (e.g. + `/v/0.7.0/`), Astro/Vite rewrite every internal link, emitted asset, and + web-worker/wasm URL to sit under that prefix. Unset ⇒ the normal root build. + +- **`release/website-rebase-links.mjs`** fixes the one thing Astro won't: + hand-written root-absolute links in the Markdown (`[sensors](/reference/sensors)`). + It rewrites them under the version prefix in the built HTML, so an archived + version never silently links back out to the live site. Links in the `/v/` + archive namespace are left alone (so a "Version history" link from inside + `/v/0.7.0/` still points at the live hub, not `/v/0.7.0/v/`). + +- **`release/website-snapshot.sh []`** ties those + together: build `` (a checkout of the release tag) under base + `/v//`, rebase links, and copy the result to ``. + +- **`release/website-ensure-base.mjs`** is a backfill safety net: it injects + `VARAR_SITE_BASE` support into an *older* tag's config that predates it. A + no-op on any tag from this change onward. + +- **`release/website-index.mjs`** generates the `/v/` hub (`index.html` + + `versions.json`) by scanning the archived version folders. + +- **`.github/workflows/website.yml`** orchestrates it. Archives are stored on an + orphan **`website-snapshots`** branch — built assets for every release, kept + off `main`'s history. The `snapshot` job (on a `v*` tag push, or manual + backfill) builds the version from a worktree of its tag and commits it to that + branch. The `deploy` job builds the live site, overlays the archive branch's + `/v/` into `dist/`, and deploys — so a freshly-tagged release ships in the same + run. + +## Backfilling old releases + +New releases are archived automatically when their tag is pushed. To archive the +releases that predate this system, run the **Website** workflow manually +(`workflow_dispatch`): + +- Leave **version** blank to archive every release tag that has no snapshot yet. +- Or set it to a single bare semver (e.g. `0.6.1`) to (re)build just that one. + +The job builds each missing version from a worktree of its tag — using its own +lockfile and toolchain, so the archive is faithful to what shipped — and pushes +the results to `website-snapshots`; the deploy then serves them. Very old tags +whose config or build differs substantially may need a hand; `website-ensure-base.mjs` +covers the common case (a config that simply predates `VARAR_SITE_BASE`). + +## Alternatives considered + +- **Subdomain per release** (`v0-7-0.varar.dev`): cleaner isolation but needs + wildcard DNS + a route per version. Path-per-release keeps one domain and one + Worker. +- **Rebuilding every tag on each deploy**: too slow and flaky. Archives are built + once and stored, then re-served on every deploy. diff --git a/release/website-ensure-base.mjs b/release/website-ensure-base.mjs new file mode 100755 index 00000000..8e915a96 --- /dev/null +++ b/release/website-ensure-base.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// Make a website Astro config honour VARAR_SITE_BASE, in place and idempotently. +// +// The current config already reads VARAR_SITE_BASE (see astro.config.mjs). This +// script exists for BACKFILL: building an *old* release tag whose config predates +// that support. Checked out at such a tag, the config ignores the env var and +// would emit root-absolute asset/link URLs that break under /v//. This +// injects the base wiring so the historical snapshot builds correctly, without +// editing the tag itself (the checkout is a throwaway worktree). +// +// Idempotent: if the config already mentions VARAR_SITE_BASE, it is left as-is, +// so running it against a modern tag is a no-op. +// +// Usage: node release/website-ensure-base.mjs +import { readFile, writeFile } from 'node:fs/promises' + +const configPath = process.argv[2] +if (!configPath) { + console.error('usage: website-ensure-base.mjs ') + process.exit(1) +} + +const src = await readFile(configPath, 'utf8') + +if (src.includes('VARAR_SITE_BASE')) { + console.log('config already base-aware; nothing to do') + process.exit(0) +} + +// Insert `base: process.env.VARAR_SITE_BASE || undefined,` as the first property +// of the defineConfig({ … }) object. Every website config back to the first +// release opens its config with this exact call, so this anchor is stable. +const anchor = 'defineConfig({' +const at = src.indexOf(anchor) +if (at === -1) { + console.error(`could not find "${anchor}" in ${configPath}`) + process.exit(1) +} + +const insertAt = at + anchor.length +const next = `${src.slice(0, insertAt)}\n base: process.env.VARAR_SITE_BASE || undefined,${src.slice( + insertAt, +)}` +await writeFile(configPath, next) +console.log(`injected VARAR_SITE_BASE support into ${configPath}`) diff --git a/release/website-index.mjs b/release/website-index.mjs new file mode 100755 index 00000000..01fb2b88 --- /dev/null +++ b/release/website-index.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node +// Generate the version-archive hub for the website. +// +// Scans for `../` snapshot subdirectories (each a +// build-accurate archive of a past release, produced by website-snapshot.sh) and +// writes two files into : +// - versions.json — the machine-readable list, newest first +// - index.html — a small, dependency-free landing page linking to each +// archived version, served at https://varar.dev/v/ +// +// The hub is assembled at deploy time, outside Astro, because it has to list +// versions that were each built from a different release tag — no single Astro +// build knows about all of them. +import { readdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const vDir = process.argv[2] +if (!vDir) { + console.error('usage: website-index.mjs ') + process.exit(1) +} + +const semver = /^(\d+)\.(\d+)\.(\d+)$/ +const entries = await readdir(vDir, { withFileTypes: true }) +const versions = entries + .filter((e) => e.isDirectory() && semver.test(e.name)) + .map((e) => e.name) + .sort((a, b) => { + const pa = a.split('.').map(Number) + const pb = b.split('.').map(Number) + return pb[0] - pa[0] || pb[1] - pa[1] || pb[2] - pa[2] + }) + +await writeFile(join(vDir, 'versions.json'), `${JSON.stringify(versions, null, 2)}\n`) + +const rows = versions + .map( + (v) => + `
  • v${v}Open →
  • `, + ) + .join('\n') + +const html = ` + + + + + Version history — Varar + + + + + +
    +

    Version history

    +

    + Build-accurate archives of past Varar releases — each is the website + exactly as it shipped, interactive editor and all. The + latest version lives at the site root. +

    +${versions.length ? `
      \n${rows}\n
    ` : '

    No archived versions yet.

    '} +
    + Looking for the newest docs? Head to varar.dev. +
    +
    + + +` + +await writeFile(join(vDir, 'index.html'), html) +console.log(`wrote ${vDir}/index.html and versions.json (${versions.length} version(s))`) diff --git a/release/website-rebase-links.mjs b/release/website-rebase-links.mjs new file mode 100755 index 00000000..896cfa3c --- /dev/null +++ b/release/website-rebase-links.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +// Rebase root-absolute links in a built website snapshot under its version +// prefix. +// +// Astro/Vite rewrite every link and asset URL *they* emit to sit under the +// configured `base` (e.g. /v/0.7.0/), but they leave hand-written root-absolute +// links in the Markdown content alone — `[sensors](/reference/sensors)` stays +// `/reference/sensors`. In a version snapshot served under /v/0.7.0/ those links +// silently escape the archive and land on the live site. This rewrites them so +// an archived version is fully self-contained: every internal navigation stays +// within /v//. +// +// Usage: node release/website-rebase-links.mjs +// is the path prefix with leading and trailing slash, e.g. /v/0.7.0/ +// +// Only /**/*.html is touched. A link is rewritten iff it is +// root-absolute (starts with a single "/", not "//" and not a full URL) and not +// already under . Fragment-only (#…) and query-only (?…) links, mailto:, +// full URLs and protocol-relative //host links are all left untouched. +import { readdir, readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const [distDir, base] = process.argv.slice(2) +if (!distDir || !base || !base.startsWith('/') || !base.endsWith('/')) { + console.error('usage: website-rebase-links.mjs ') + process.exit(1) +} + +// Matches href="/…" / src="/…" (single or double quotes) where the path is +// root-absolute but not protocol-relative (//) and not in the /v/ archive +// namespace. Skipping all of /v/ (not just the current base) keeps the rewrite +// idempotent AND leaves cross-archive links alone: from inside /v/0.7.0/, a +// "Version history" link to /v/ must stay pointing at the live hub, never become +// /v/0.7.0/v/. External URLs (http…, //host) are left untouched too. +const linkRe = /(\b(?:href|src)=)(["'])\/(?!\/|v\/)/g + +async function* htmlFiles(dir) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const full = join(dir, entry.name) + if (entry.isDirectory()) yield* htmlFiles(full) + else if (entry.name.endsWith('.html')) yield full + } +} + +let rewritten = 0 +for await (const file of htmlFiles(distDir)) { + const src = await readFile(file, 'utf8') + const next = src.replace(linkRe, (_m, attr, quote) => `${attr}${quote}${base}`) + if (next !== src) { + await writeFile(file, next) + rewritten++ + } +} +console.log(`rebased root-absolute links under ${base} in ${rewritten} file(s)`) diff --git a/release/website-snapshot.sh b/release/website-snapshot.sh new file mode 100755 index 00000000..f3fe1234 --- /dev/null +++ b/release/website-snapshot.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Build a build-accurate website snapshot for one release version. +# +# Runs against whatever the working tree is currently checked out at (a release +# tag) and writes the finished, self-contained archive — links rebased under +# /v// — to . The whole archive, interactive editor and all, +# is exactly what shipped for that release; only URLs are re-homed under the +# version prefix. +# +# Usage: release/website-snapshot.sh [] +# bare semver, e.g. 0.7.0 (no leading "v") +# emptied and filled with the snapshot (e.g. .../v/0.7.0) +# repo checkout to BUILD from; defaults to this script's repo. +# Backfill passes a worktree of the release tag here, so an old +# version is built from its own source while the snapshot *tooling* +# (this script and its helpers) always comes from the current repo. +# +# The caller is responsible for checking out the right ref into and +# running `pnpm install` there beforehand (a historical tag brings its own +# lockfile). +set -euo pipefail + +VERSION="${1:-}" +OUT="${2:-}" +[[ -n "$VERSION" && -n "$OUT" ]] || { + echo "usage: release/website-snapshot.sh []" >&2 + exit 1 +} +[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "not a bare semver version: $VERSION" >&2 + exit 1 +} + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRC_ROOT="$(cd "${3:-$REPO_ROOT}" && pwd)" +BASE="/v/${VERSION}/" + +# Backfill safety net: teach an older tag's config to honour VARAR_SITE_BASE. +# A no-op on any tag from this change onward. +node "$REPO_ROOT/release/website-ensure-base.mjs" \ + "$SRC_ROOT/typescript/packages/website/astro.config.mjs" + +echo "building website snapshot v$VERSION under base $BASE (from $SRC_ROOT)" +(cd "$SRC_ROOT/typescript" && VARAR_SITE_BASE="$BASE" pnpm --filter @varar/website... build) + +DIST="$SRC_ROOT/typescript/packages/website/dist" +node "$REPO_ROOT/release/website-rebase-links.mjs" "$DIST" "$BASE" + +rm -rf "$OUT" +mkdir -p "$OUT" +cp -R "$DIST/." "$OUT/" +echo "snapshot v$VERSION written to $OUT" diff --git a/typescript/packages/website/astro.config.mjs b/typescript/packages/website/astro.config.mjs index c0d9f9c6..8da56a11 100644 --- a/typescript/packages/website/astro.config.mjs +++ b/typescript/packages/website/astro.config.mjs @@ -40,9 +40,17 @@ const restorePrefs = () => ({ }, }) +// Version snapshots (build-accurate archives of past releases) are served under +// a path prefix, e.g. https://varar.dev/v/0.7.0/. The release-tag snapshot build +// sets VARAR_SITE_BASE to that prefix; Astro/Vite then rewrite every internal +// link, emitted asset, and web-worker URL to live under it. Unset (the live +// site build) means base '/', the normal root deployment. +const base = process.env.VARAR_SITE_BASE || undefined + // https://astro.build/config export default defineConfig({ site: 'https://varar.dev', + base, // The project was renamed var → varar; these three pages carried the old name // in their URL. Keep the published links working. redirects: { @@ -56,6 +64,12 @@ export default defineConfig({ starlight({ title: 'Varar', tableOfContents: true, + // A version snapshot (base set) is an archive of a past release. Keep it + // out of search so the twelve-plus historical copies don't compete with + // the live docs for the same queries; readers reach them via direct links. + head: base + ? [{ tag: 'meta', attrs: { name: 'robots', content: 'noindex, follow' } }] + : [], plugins: [ // Generate /llms.txt (a curated index) and /llms-full.txt (every doc // page concatenated as clean Markdown) at build time. Agents are a @@ -82,6 +96,7 @@ export default defineConfig({ ], components: { ThemeSelect: './src/components/ThemeSelect.astro', + Footer: './src/components/Footer.astro', }, social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/varar-dev/varar' }], sidebar: [ diff --git a/typescript/packages/website/src/components/Footer.astro b/typescript/packages/website/src/components/Footer.astro new file mode 100644 index 00000000..f36decaa --- /dev/null +++ b/typescript/packages/website/src/components/Footer.astro @@ -0,0 +1,34 @@ +--- +/** + * Starlight `Footer` override: renders the built-in footer (pagination, edit + * link, last-updated) and appends a link to the version archive at /v/. + * + * Past releases are kept as build-accurate snapshots served under + * /v// (see doc/website-versioning.md and .github/workflows/website.yml); + * this footer link is how a reader on the live site discovers them. It points at + * the generated archive hub (/v/), which lists every archived version. + */ +import Default from '@astrojs/starlight/components/Footer.astro' +--- + + + + + + diff --git a/typescript/pnpm-workspace.yaml b/typescript/pnpm-workspace.yaml index 60133d4e..cc9b4ccb 100644 --- a/typescript/pnpm-workspace.yaml +++ b/typescript/pnpm-workspace.yaml @@ -18,7 +18,7 @@ packages: # Denying them skips a native compile that nothing uses and drops the # requirement for a C toolchain to install this repo. # -# workerd and sharp@0.34 arrive with wrangler, which the deploy-website CI job +# workerd and sharp@0.34 arrive with wrangler, which the website deploy CI job # installs on the fly (cloudflare/wrangler-action's `pnpm add wrangler@4`) — an # install that pnpm fails outright while they are undecided. Both are a NO: # workerd is the local `wrangler dev` runtime and sharp is miniflare's image From 967efbf8b5c63a6349744fb1fa21b8c161f986fa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 13:47:42 +0000 Subject: [PATCH 2/2] chore(website): warn on archived versions and keep them out of search Only the live docs should rank in search, and a reader who lands on an old version should know it. - Archived pages already carry noindex; extend it to the generated /v/ hub, so the live site is the single indexed version (the sitemap lists live pages only). - Add a Banner override that, on a snapshot build, shows an old-version warning linking to the same page on the live site. The link is an absolute URL (via Astro.site) so the link rebaser can't pull it back into the archive. Inert on the live build. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014FaBNvwetMrvHtc5PqVRfa --- doc/website-versioning.md | 17 ++++- release/website-index.mjs | 3 + typescript/packages/website/astro.config.mjs | 5 +- .../website/src/components/Banner.astro | 63 +++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 typescript/packages/website/src/components/Banner.astro diff --git a/doc/website-versioning.md b/doc/website-versioning.md index 3867a3a6..abb46244 100644 --- a/doc/website-versioning.md +++ b/doc/website-versioning.md @@ -8,6 +8,13 @@ interactive `` components running exactly the `@varar/varar` code that shipped in that release. A "Version history" link in the site footer, and the hub page at [`/v/`](https://varar.dev/v/), list every archived version. +Only the live site is meant to rank in search: every archived page (and the +`/v/` hub) carries ``, and the +site sitemap lists live pages only. A reader who lands on an archived page — via +a direct link or a bookmark — gets a warning banner across the top ("You're +reading the docs for Varar vX, an older release. View the latest →") linking to +the same page on the live site. + ## Why build-accurate, not content-only There is a Starlight plugin (`starlight-versions`) that snapshots docs *content* @@ -25,7 +32,15 @@ The pieces: - **`packages/website/astro.config.mjs`** reads `VARAR_SITE_BASE`. When set (e.g. `/v/0.7.0/`), Astro/Vite rewrite every internal link, emitted asset, and - web-worker/wasm URL to sit under that prefix. Unset ⇒ the normal root build. + web-worker/wasm URL to sit under that prefix, and the build gets the `noindex` + robots tag. Unset ⇒ the normal, indexable root build. + +- **`packages/website/src/components/Banner.astro`** and **`Footer.astro`** are + Starlight overrides. In a snapshot build (base set) the Banner shows the + old-version warning, its "view the latest" link built as an absolute URL via + `Astro.site` so the link rebaser leaves it pointing at the live site. The + Footer adds the "Version history" link to `/v/`. Both are inert on the live + build. - **`release/website-rebase-links.mjs`** fixes the one thing Astro won't: hand-written root-absolute links in the Markdown (`[sensors](/reference/sensors)`). diff --git a/release/website-index.mjs b/release/website-index.mjs index 01fb2b88..ea9eb1f1 100755 --- a/release/website-index.mjs +++ b/release/website-index.mjs @@ -50,6 +50,9 @@ const html = ` name="description" content="Build-accurate archives of past releases of the Varar website." /> + +