From a0d0c3045287143e3123b975587b80bde9c187ad Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 26 Aug 2026 22:33:13 +1000 Subject: [PATCH 1/7] feat: make the docs site deployable to Cloudflare the same way the marketing sites are MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the wrangler config the Git-integration path needs, so this repo can publish either way: `just deploy` by hand, or Cloudflare Workers Builds on push once the repo is connected in the dashboard. `just deploy` is now a bare `wrangler deploy` — the name, compatibility date and assets directory live in wrangler.jsonc instead of being repeated on the command line, matching the marketing repos. The .assetsignore is the part that matters. Running a real export showed `mint export` copies repo files into its output (justfile, scripts/, LICENSE) and adds its own double-click-to-serve helpers (serve.js, Start Docs.bat, Start Docs.command). Published as-is, all of those would be reachable over HTTP. Because dist/ is regenerated on every build, the ignore file is committed at the repo root and copied in by `just export`. Two things the export decided for us rather than the other way round: it carries no 404.html, so not_found_handling stays at its default instead of pointing at a page that does not exist; and the unzip step falls back to python3 -m zipfile, since `unzip` is not guaranteed in a CI image. dist/ and export.zip are now gitignored — the export is ~47MB, most of it the Next.js bundle. --- .assetsignore | 18 ++++++++++++++++++ .gitignore | 4 ++++ justfile | 6 ++++-- wrangler.jsonc | 23 +++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .assetsignore create mode 100644 wrangler.jsonc diff --git a/.assetsignore b/.assetsignore new file mode 100644 index 0000000..fe213fd --- /dev/null +++ b/.assetsignore @@ -0,0 +1,18 @@ +# Files that must not be served as part of the docs site. +# +# `mint export` copies repo files into the export and adds its own +# local-serving helpers, so without this they end up published: +# serve.js, Start Docs.* Mintlify's double-click-to-serve helpers +# justfile, scripts/ copied out of this repo by the exporter +# LICENSE, README.md repo files, not site content +# +# This file is committed at the repo root and copied into dist/ by +# `just export`, because dist/ is regenerated on every build. +serve.js +Start Docs.bat +Start Docs.command +justfile +scripts +LICENSE +README.md +.assetsignore diff --git a/.gitignore b/.gitignore index e5b72bc..d9fca57 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ Thumbs.db # Logs *.log npm-debug.log* + +# Mintlify export output (built by `just export`) +export.zip +dist/ diff --git a/justfile b/justfile index fd5b06a..b13b76a 100644 --- a/justfile +++ b/justfile @@ -18,9 +18,11 @@ run: export: npx --yes mint@latest export rm -rf dist - unzip -q export.zip -d dist + mkdir -p dist + unzip -q -o export.zip -d dist || python3 -m zipfile -e export.zip dist + cp .assetsignore dist/.assetsignore # Deploy the docs site to Cloudflare [group("deploy")] deploy: export - npx --yes wrangler@latest deploy --assets dist --name docs-privacycommand --compatibility-date 2026-05-01 + npx --yes wrangler@latest deploy diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..ffe7658 --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,23 @@ +{ + // Static-assets-only Worker: no `main` entry point, so there is no Worker + // script — Cloudflare just serves the exported files. Asset requests are + // unmetered. + // + // `dist/` is the unpacked `mint export` output and is NOT in git; build it + // with `just export` locally, or with this build command in Workers Builds: + // + // npx --yes mint@latest export && rm -rf dist && mkdir -p dist \ + // && (unzip -q -o export.zip -d dist || python3 -m zipfile -e export.zip dist) \ + // && cp .assetsignore dist/.assetsignore + // + // The Mintlify export carries no 404.html, so not_found_handling is left at + // its default rather than pointing at a page that does not exist. + "name": "docs-privacycommand", + "compatibility_date": "2026-05-01", + "observability": { + "enabled": true + }, + "assets": { + "directory": "./dist" + } +} From 159df625fd8505e01910db826aadf97eda2a89f6 Mon Sep 17 00:00:00 2001 From: adamXbot <111877622+adamXbot@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:56:23 +1000 Subject: [PATCH 2/7] build: add npm run build so Workers Builds can produce dist/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrangler config points assets.directory at dist/, which is gitignored and only exists after mint export runs. A connected repo therefore failed with "the directory specified by assets.directory does not exist". Workers Builds has no default build command, so this cannot be fully automatic — but naming the script "build" makes the dashboard setting a plain `npm run build` instead of a long shell one-liner. just export now delegates to the same script so the steps are defined once. Co-Authored-By: Claude Fable 5 --- justfile | 6 +----- package.json | 1 + wrangler.jsonc | 11 +++++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/justfile b/justfile index b13b76a..a7a122c 100644 --- a/justfile +++ b/justfile @@ -16,11 +16,7 @@ run: # Build the Mintlify static export into dist/ [group("deploy")] export: - npx --yes mint@latest export - rm -rf dist - mkdir -p dist - unzip -q -o export.zip -d dist || python3 -m zipfile -e export.zip dist - cp .assetsignore dist/.assetsignore + npm run build # Deploy the docs site to Cloudflare [group("deploy")] diff --git a/package.json b/package.json index 538465d..ccc1b02 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "npx --yes mint dev", + "build": "npx --yes mint@latest export && rm -rf dist && mkdir -p dist && (unzip -q -o export.zip -d dist || python3 -m zipfile -e export.zip dist) && cp .assetsignore dist/.assetsignore", "check": "node scripts/check-docs.mjs", "linkcheck": "lychee --base . './**/*.mdx' './**/*.md'" }, diff --git a/wrangler.jsonc b/wrangler.jsonc index ffe7658..a571542 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -3,12 +3,11 @@ // script — Cloudflare just serves the exported files. Asset requests are // unmetered. // - // `dist/` is the unpacked `mint export` output and is NOT in git; build it - // with `just export` locally, or with this build command in Workers Builds: - // - // npx --yes mint@latest export && rm -rf dist && mkdir -p dist \ - // && (unzip -q -o export.zip -d dist || python3 -m zipfile -e export.zip dist) \ - // && cp .assetsignore dist/.assetsignore + // `dist/` is the unpacked `mint export` output and is NOT in git. It is + // built by `npm run build`. Workers Builds has NO default build command, so + // a connected repo must set its build command to `npm run build` in the + // Cloudflare dashboard, or the deploy fails with "directory does not exist". + // `just export` and `just deploy` call the same script. // // The Mintlify export carries no 404.html, so not_found_handling is left at // its default rather than pointing at a page that does not exist. From 95b210dfb9bfd009cfe70945e9096c34e716031c Mon Sep 17 00:00:00 2001 From: adamXbot <111877622+adamXbot@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:58:47 +1000 Subject: [PATCH 3/7] docs: replace stale Mintlify-hosting instructions with the Workers deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These READMEs still walked through connecting the repo at dashboard.mintlify.com and CNAMEing to cname.mintlify.app. That is not how these sites publish any more — production is a Cloudflare Worker serving the static export. Drops the standalone deploy sections in favour of a two-line statement near the top, matching the website repos, and keeps the workflow list under CI where it belongs. Also corrects a published page (develop/docs-site.mdx) and SPEC.md. Co-Authored-By: Claude Fable 5 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 78ab4a3..2ed7c23 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ Source for the [privacycommand](https://github.com/privacykey/privacycommand) documentation site, built with [Mintlify](https://mintlify.com). -Published at `docs.privacycommand.privacykey.org`. +Production is a Cloudflare Worker serving the static export as assets +([`wrangler.jsonc`](wrangler.jsonc)). `just deploy` builds and publishes it. +**Hostname:** `docs.privacycommand.privacykey.org` *(DNS not configured yet)* ## Local preview From bc886cfa45940c21063b2151e802c2b2aeef35f2 Mon Sep 17 00:00:00 2001 From: adamXbot <111877622+adamXbot@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:02:45 +1000 Subject: [PATCH 4/7] =?UTF-8?q?ci:=20unbreak=20lychee=20=E2=80=94=20drop?= =?UTF-8?q?=20rejected=20--base=20.,=20exclude=20bot-blocking=20host?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The link check has failed on main in every docs repo since mid-August. Current lychee releases reject `--base .` outright ("base must either be a full URL or an absolute local path"), so the job died before checking anything. Replaced with --root-dir plus an external-only scheme filter; internal links and anchors are already covered by `npm run check`, which runs first in the same job. Separately, dash.cloudflare.com returns 403 to CI, failing docs-mantis on a link that is fine in a browser. Excluded that host rather than accepting 403 globally, which would mask genuinely forbidden links. Co-Authored-By: Claude Fable 5 --- .github/workflows/linkcheck.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 67f139b..108142e 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -43,6 +43,12 @@ jobs: key: cache-lychee-${{ github.sha }} restore-keys: cache-lychee- + # External links only (--scheme): internal links and anchors are covered by + # `npm run check` above, and extensionless Mintlify paths are not files. + # `--base .` is rejected by current lychee (base must be a URL or absolute + # path); --root-dir resolves root-relative links instead. + # dash.cloudflare.com bot-blocks CI with 403 — excluded rather than accepting + # 403 globally, which would mask genuinely forbidden links. - name: Run lychee uses: lycheeverse/lychee-action@v2 with: @@ -50,9 +56,12 @@ jobs: --cache --max-cache-age 1d --no-progress - --base . + --scheme https + --scheme http + --root-dir ${{ github.workspace }} --exclude-path images --exclude-path .github + --exclude 'dash\.cloudflare\.com' --accept 200,206,429 './**/*.mdx' './**/*.md' From 5da87be1a8f54a9c1c76701fb09c39a5b4eac268 Mon Sep 17 00:00:00 2001 From: adamXbot <111877622+adamXbot@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:07:28 +1000 Subject: [PATCH 5/7] ci: fix the links lychee was right about, exclude the ones it was not With lychee actually running again it found real breakage. docs-privacytracker linked to `privacykey/privacytracker-docs` in eight places; the repo was renamed to `docs-privacytracker` and GitHub's redirect had been hiding it from readers but not from the checker. The rest are false positives, excluded with the reason recorded next to each: the docs domains have no DNS yet, privacykey/privacysentinel is a private repo so anonymous CI gets a 404 on a correct link, and shell.azure.com bot-blocks CI. Verified locally: all four repos now report 0 errors. Co-Authored-By: Claude Fable 5 --- .github/workflows/linkcheck.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 108142e..9b179a3 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -49,6 +49,7 @@ jobs: # path); --root-dir resolves root-relative links instead. # dash.cloudflare.com bot-blocks CI with 403 — excluded rather than accepting # 403 globally, which would mask genuinely forbidden links. + # docs.[a-z]+.privacykey.org: no docs domain has DNS yet; drop this exclusion once they resolve - name: Run lychee uses: lycheeverse/lychee-action@v2 with: @@ -62,6 +63,7 @@ jobs: --exclude-path images --exclude-path .github --exclude 'dash\.cloudflare\.com' + --exclude 'docs\.[a-z]+\.privacykey\.org' --accept 200,206,429 './**/*.mdx' './**/*.md' From 70cbd1eec64e5ce916c579feb41b9b5f9d89eebc Mon Sep 17 00:00:00 2001 From: adamXbot <111877622+adamXbot@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:10:12 +1000 Subject: [PATCH 6/7] ci: exclude loopback URLs from the link check The docs use http://localhost:3000 in examples. Those resolved on a dev machine with the preview running, which is why this only surfaced in CI, where nothing listens on the port and lychee reported connection refused. Co-Authored-By: Claude Fable 5 --- .github/workflows/linkcheck.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 9b179a3..7a32688 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -49,6 +49,7 @@ jobs: # path); --root-dir resolves root-relative links instead. # dash.cloudflare.com bot-blocks CI with 403 — excluded rather than accepting # 403 globally, which would mask genuinely forbidden links. + # localhost/loopback URLs are documentation examples, not live links. # docs.[a-z]+.privacykey.org: no docs domain has DNS yet; drop this exclusion once they resolve - name: Run lychee uses: lycheeverse/lychee-action@v2 @@ -62,6 +63,8 @@ jobs: --root-dir ${{ github.workspace }} --exclude-path images --exclude-path .github + --exclude-loopback + --exclude 'localhost' --exclude 'dash\.cloudflare\.com' --exclude 'docs\.[a-z]+\.privacykey\.org' --accept 200,206,429 From 42613ae7b8672bf6fa42b9b678085cf91049cb66 Mon Sep 17 00:00:00 2001 From: adamXbot <111877622+adamXbot@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:13:14 +1000 Subject: [PATCH 7/7] ci: give the link check a longer timeout and retries lychee exits non-zero on a timeout as well as on a broken link, so one slow third-party host (tauri.app, in the run that prompted this) fails the whole PR with zero actual errors reported. 30s and five retries instead. Co-Authored-By: Claude Fable 5 --- .github/workflows/linkcheck.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 7a32688..2aab5f4 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -50,6 +50,8 @@ jobs: # dash.cloudflare.com bot-blocks CI with 403 — excluded rather than accepting # 403 globally, which would mask genuinely forbidden links. # localhost/loopback URLs are documentation examples, not live links. + # Generous timeout/retries: lychee exits non-zero on a timeout as well as a + # broken link, so a slow third-party host would otherwise block every PR. # docs.[a-z]+.privacykey.org: no docs domain has DNS yet; drop this exclusion once they resolve - name: Run lychee uses: lycheeverse/lychee-action@v2 @@ -58,6 +60,9 @@ jobs: --cache --max-cache-age 1d --no-progress + --timeout 30 + --max-retries 5 + --retry-wait-time 3 --scheme https --scheme http --root-dir ${{ github.workspace }}