08 · Publishing & Deploy
← Docs home · Task: publish a section into docs/, bust caches,
and ship it to Cloudflare Pages.
Source of truth:
scripts/workflows/publish.py,bump-cache-version,wrangler.toml. The README’s “GitHub Pages” note is stale — the live site is Cloudflare Pages.
Step 1 — publish to docs/
publish.py is pipeline step 13, but also runs standalone:
python scripts/workflows/publish.py --project "Calculus Volume 1" --section 3.1
python scripts/workflows/publish.py --project "Calculus Volume 1" # all sections
What it does, in order:
- Verify HTML (
_verify_html) — broken links, unclosed tags, malformed LaTeX → writes a.verify_okstamp on pass. - Copy
projects/<Book>/html/<sec>.html→docs/<slug>/chapter-N-*/<sec>.htmland referenced images →docs/images/. - Apply figure swaps (the figure-animator survival guard) —
<img>→<figure-anim-wrap>/<figure-static-wrap>fromfigure_animations.yaml. - Group consecutive figures into
.figure-rowgrids. - Sync the navbar (
sync_docs_script_dropdowns) and regenerate the collection index indocs/index.html.
Useful flags:
| Flag | Effect |
|---|---|
--dry-run |
Report only; write nothing. |
--sections 2.1 2.3 |
Publish only those sections. |
--auto-render |
Invoke figure_animate render --execute after copy (canonical full-pipeline flow; needs Docker). |
--slides |
Also build the Slidev deck (see 06). |
--sweep-only |
GC orphaned static PNGs (run after figure renders settle). |
⚠️ Path-depth gotcha (memory
project_publish_path_depth): the asset-rewrite depth inpublish.pymust besub.count('/') + 1. A past commit bumped it to+2and silently broke CSS on chapter-subdir pages. If chapter pages render unstyled after a publish change, check this first.
Step 2 — bump cache version
Pages send Cache-Control: no-cache on .html (see docs/_headers), but
versioned assets (?v=YYYYMMDD) own their own cache lifetime. After any
CSS/JS or figure re-render, bump the token.
Say “bump cache version” → the bump-cache-version skill sweeps three surfaces
and verifies the old token is gone:
- The skeleton template.
projects/*/html/*.html.docs/*/*.html(+ chapter subdirs).
Token format: YYYYMMDD (today), or YYYYMMDD<letter> for same-day re-bumps.
Per-figure MP4 tokens in figure_animations.yaml (cache_buster:) and the
HTML ?v= query must be bumped together.
Step 3 — deploy to Cloudflare Pages
git pushdoes NOT deploy. (Memoryproject_pages_deploy.) ThebookshelfPages project has no Git integration — it deploys by direct upload.
⚠️ The production branch is
desktop, notpreview. Deploy todesktopto update the live public site (oerbookshelf.app+getstarted.oerbookshelf.app).--branch previewonly updates the throwawaypreview.bookshelf-9l4.pages.devalias and will leave the real site stale. Verify the production branch withwrangler pages deployment list --project-name bookshelf --environment production.
# PRODUCTION (updates oerbookshelf.app):
wrangler pages deploy docs --project-name bookshelf --branch desktop --commit-dirty=true
# staging only (preview.bookshelf-9l4.pages.dev):
# wrangler pages deploy docs --project-name bookshelf --branch preview
- Deploy root is
docs/(pages_build_output_dir = "docs"inwrangler.toml). - The project is
bookshelf; production domainoerbookshelf.app(pages.dev:bookshelf-9l4.pages.dev). - Runtime bindings (D1
DB, R2DECK_FILES,GH_*vars) come fromwrangler.toml; theGITHUB_TOKENsecret is set out-of-band:wrangler pages secret put GITHUB_TOKEN.
Backend prerequisites (one-time)
- D1 database
bookshelf-aiexists; migrations applied:
(Migrations:wrangler d1 migrations apply bookshelf-aimigrations/0001…0006— AI-chat rate limit, slide decks, uploads, active deck, intake submissions, intake source-file columns.) The intake tables can also be applied directly:wrangler d1 execute bookshelf-ai --file=migrations/0005_intake_submissions.sql --remote wrangler d1 execute bookshelf-ai --file=migrations/0006_intake_source_file.sql --remote - R2 buckets exist:
wrangler r2 bucket create bookshelf-decks # public slide decks wrangler r2 bucket create bookshelf-intake # PRIVATE intake source uploads - Admin routes (
/admin/slides,/admin/intake) sit behind Cloudflare Access — seedocs/ops/admin-access.md. The client-intake form is documented in 11 · Client Intake.
Docs site
These docs are published as their own rendered site at docs.oerbookshelf.app.
- Source:
docs/docs/*.md. Build:npm run docs(scripts/workflows/build_docs.mjs) renders each.md→ themed.htmlindocs/docs/(markdown-it + the house theme + a sidebar nav). Re-run after any.mdedit; the.htmlare committed and deployed with the rest ofdocs/. - Routing:
functions/_middleware.jsrewrites the docs subdomain root onto the docs —docs.oerbookshelf.app/<page>→/docs/<page>— so URLs are clean (no/docs/). The same files also stay reachable atoerbookshelf.app/docs/. Every other host passes through untouched. Links are extensionless so Pages serves them 200 (no.html→ clean-URL 308). - One-time wiring (dashboard): add
docs.oerbookshelf.appas a custom domain on the Pages project (Pages → bookshelf → Custom domains). Cloudflare creates the DNS record automatically; the middleware does the rest.
Edit
.md→npm run docs→ deploy. Forgetting the rebuild ships stale HTML.
Deploy ordering rule
If a section uses raSHio callouts/walkthroughs that link the live tool site, deploy raSHio first, then ship the book page. Same for any externally-linked companion — the link target must be live before the page that points at it.
Quick checklist
[ ] preflight green (see 03 · Content Pipeline)
[ ] publish.py (--auto-render if figures) → docs/ updated, .verify_ok written
[ ] figures/CSS changed? → bump-cache-version
[ ] external links (raSHio) live?
[ ] wrangler pages deploy docs --project-name bookshelf --branch desktop --commit-dirty=true
[ ] spot-check oerbookshelf.app (--branch desktop = production)
[ ] commit (only if asked — see 09 · Maintenance)