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..abb46244 --- /dev/null +++ b/doc/website-versioning.md @@ -0,0 +1,92 @@ +# 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. + +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* +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, 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)`). + 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..ea9eb1f1 --- /dev/null +++ b/release/website-index.mjs @@ -0,0 +1,163 @@ +#!/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..cbee3c7d 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,10 @@ 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 +94,8 @@ export default defineConfig({ ], components: { ThemeSelect: './src/components/ThemeSelect.astro', + Footer: './src/components/Footer.astro', + Banner: './src/components/Banner.astro', }, social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/varar-dev/varar' }], sidebar: [ diff --git a/typescript/packages/website/src/components/Banner.astro b/typescript/packages/website/src/components/Banner.astro new file mode 100644 index 00000000..c0174b14 --- /dev/null +++ b/typescript/packages/website/src/components/Banner.astro @@ -0,0 +1,63 @@ +--- +/** + * Starlight `Banner` override. + * + * Renders the built-in frontmatter banner (unchanged), and — only in a version + * snapshot build (VARAR_SITE_BASE set, so `import.meta.env.BASE_URL` is + * `/v//`) — prepends a warning that the reader is on an archived + * release, linking to the same page on the live site. + * + * The "view the latest" link is built as an ABSOLUTE URL via `Astro.site` on + * purpose: a root-absolute `/reference/…` link would be rewritten back under the + * version prefix by release/website-rebase-links.mjs, sending the reader in a + * circle. An absolute https URL is left alone and always points at the live + * site. + */ +import Default from '@astrojs/starlight/components/Banner.astro' + +const base = import.meta.env.BASE_URL +const archived = base !== '/' +const version = archived ? (base.match(/\/v\/([^/]+)\//)?.[1] ?? null) : null + +// The same page on the live site: drop the /v// prefix from this +// page's path, then resolve against the configured site origin. +const pathname = Astro.url.pathname +const latestPath = archived && pathname.startsWith(base) ? `/${pathname.slice(base.length)}` : '/' +const latestHref = Astro.site ? new URL(latestPath, Astro.site).href : latestPath +--- + +{ + version && ( +
    + + You’re reading the docs for Varar v{version}, an older + release. + + View the latest → +
    + ) +} + + + + 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