Gå til hovedinnhold

CDN asset offloading (Cloudflare R2)

The documentation sites deploy to GitHub Pages by pushing the built static output to neokapi.github.io. A few asset families are large, immutable, and fetched at runtime rather than needed to render a page:

FamilyApprox. sizeWhere it's used
Playground WASM (kapi-cli.wasm + .gz, kapi.wasm, pdfium.wasm, wasm_exec.js)~125 MBThe Lab / KLF playground, PDF Lab
Vision ONNX models (PP-OCRv5 + PP-DocLayoutV3)~155 MBThe Vision Lab
Walkthrough videos (.webm light/dark + .jpg posters)~85 MB kapi / ~55 MB bowrainThemedVideo embeds

Bundling these into the Pages artifact makes every deploy slow and forced an awkward workaround for the ~132 MB layout model (split into sub-100 MB parts to fit the GitHub Pages per-file limit). Offloading them to Cloudflare R2 — free tier (10 GB storage, free egress), served behind a custom domain with CDN caching and CORS — removes the bulk from the Pages artifact and lets the models ship whole.

Opt-in by design

Everything here is inert until configured. The site reads the CDN origin from a build-time env var, DOCS_CDN_URL, surfaced to the frontend as the cdnBaseUrl Docusaurus customField. When it is empty — the default, and the local-dev case — every asset resolves same-origin exactly as before. Nothing changes until the DOCS_CDN_URL repo variable and R2 secrets are set.

The frontend routing lives in one shared helper, @neokapi/docs-shared's cdn.ts (readCdnConfig / cdnEnabled / cdnHref), consumed by:

  • packages/docs-shared/src/ThemedVideo.tsx — video + poster sources
  • web/src/components/KapiPlayground/config.tswasmUrl / wasmExecUrl
  • web/src/pages/lab/vision.tsx — the Vision Lab modelBase

Bucket layout

One bucket backs both sites; objects are scoped per-site to avoid collisions. The WASM is versioned by commit sha so it can be cached immutably without a new deploy serving a stale binary.

<bucket>/
kapi/
wasm/<git-sha>/{kapi-cli.wasm, kapi-cli.wasm.gz, kapi.wasm, pdfium.wasm, wasm_exec.js}
models/vision/{ppocrv5_det.onnx, ppocrv5_rec.onnx, ppocrv5_dict.txt, ppdoclayoutv3.onnx}
icu/<icu-version>/icu_capi.wasm # ICU4X (Segmentation Lab), served application/wasm
video/... # .webm + .jpg posters, mirroring web/static/video/
bowrain/
video/... # mirroring bowrain/web/docs/static/video/

Served URLs: ${DOCS_CDN_URL}/kapi/wasm/<sha>/kapi-cli.wasm, etc.

Cloudflare setup (one-time)

  1. Put the domain (bowrain.cloud) on a Cloudflare zone (free plan is fine).
  2. Create an R2 bucket and bind a custom domain (e.g. cdn.bowrain.cloud) to it — required for CDN caching, CORS, and custom headers; the r2.dev URL is rate-limited and dev-only. Disable the r2.dev public URL.
  3. Add a cache rule on the custom domain: Cache Everything, honoring the origin Cache-Control (the publish script sets immutable on wasm/models and 1-day on videos).
  4. Apply the CORS policy (so browser fetch() of models/wasm works cross-origin):
    aws s3api put-bucket-cors --bucket "$R2_BUCKET" \
    --cors-configuration file://scripts/r2-cors.json \
    --endpoint-url "$R2_ENDPOINT"
  5. Lifecycle: because each docs build writes wasm under a fresh <git-sha>/ prefix, add an R2 lifecycle rule to expire objects under kapi/wasm/ after ~30 days. The live site always references a recent sha (it redeploys on every push to main), so expiring old prefixes only affects long-stale deploys.

Credentials

Create an R2 S3-compatible API token scoped to the bucket. Both the publish script and CI read it from standard env vars:

Env varValue
R2_BUCKETbucket name
R2_ENDPOINThttps://<account-id>.r2.cloudflarestorage.com
AWS_ACCESS_KEY_IDR2 access key id
AWS_SECRET_ACCESS_KEYR2 secret access key

In GitHub: set DOCS_CDN_URL, R2_BUCKET, R2_ENDPOINT as repository variables, and R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY as repository secrets.

Publishing

WASM is rebuilt on every docs build, so CI publishes it automatically (the docs-kapi.yml build job syncs to kapi/wasm/<sha>/ and drops it from the artifact when DOCS_CDN_URL is set). The other families are published out-of-band, mirroring the old docs-assets release flow: the vision models are pre-trained artifacts pinned in the vision-models-v1 GitHub release (the publish target just re-uploads them to R2 — rerun only when that release changes), and the videos are produced on the desktop by the harness:

Publishing assets to R2

R2 is the single source of truth for these assets — the docs-assets / bowrain-docs-assets GitHub releases are retired. Publish from the desktop, where the harness renders the videos/screenshots and make fetch-vision-models stages the model set (the vision models themselves are still pinned in the vision-models-v1 release — the publish target just re-uploads them to R2). Needs gh + the aws CLI + the R2_* env vars:

make publish-cdn-all # videos + images + vision models, kapi & bowrain → R2

Order matters: publish (or run the individual targets below) before setting the DOCS_CDN_URL repo variable. Once the variable is set, CI builds the sites pointing at the CDN (for push and same-repo PRs), so the deployed and preview sites expect the assets on R2 — publish first or they 404. (WASM is the exception: CI builds and publishes it, versioned by sha, in the same run.)

Individual targets

# when assets change (needs the R2_* env vars above + aws CLI):
make publish-cdn-vision-models # ONNX models → kapi/models/vision/<web/models.version>/
make publish-cdn-icu # ICU4X seg wasm → kapi/icu/<ver>/icu_capi.wasm
make publish-cdn-videos # web/static/video → kapi/video/
make publish-cdn-bowrain-videos # bowrain videos → bowrain/video/
make publish-cdn-images # web/static/img → kapi/img/
make publish-cdn-bowrain-images # bowrain images → bowrain/img/
make publish-cdn-wasm # optional manual wasm push (CI does this in deploy)

The vision model set is versioned: kapi/models/vision/<version>/, with the version pinned in the committed web/models.version. To ship a new model set, publish it under a new version and bump that file — a PR doing so previews the new models automatically (the Vision Lab reads the version from the build).

All of these call scripts/publish-cdn-assets.sh <family>, which sets the right Content-Type and Cache-Control per family. The pre-gzipped kapi-cli.wasm.gz is uploaded as an opaque blob with no Content-Encoding — the runtime self-inflates it via DecompressionStream, so a Content-Encoding: gzip header would make the browser double-inflate and fall back to the 71 MB raw binary.

CI behavior

docs-kapi.yml / docs-bowrain.yml compute a job-level CDN_URL = DOCS_CDN_URL on pushes only (PR previews always stay same-origin, to avoid R2 churn and secret dependence). When CDN_URL is set, the video- and model-staging steps are skipped, the wasm is synced to R2 and removed from the artifact, and the site is built with DOCS_CDN_URL + DOCS_CDN_VERSION (= commit sha) so it points at the CDN.