RUNBOOK — blog-quarto

Operational reference for the Quarto blog at pietstam.nl, hosted on Codeberg Pages via Forgejo CI.


Architecture overview

Local (Mac)                    Forgejo CI (Codeberg)          Live site
──────────────────────         ──────────────────────         ─────────
posts/, talks/, *.qmd          push to main triggers:
_freeze/ (frozen outputs)  →   quarto render               →  pages branch
data/zotero-cache.json         force-push _site/ to pages      pietstam.nl
scripts/fetch-zotero.js

Key branches: - main — source; edit here, never rebase - pages — auto-generated by CI; never edit manually

Required Forgejo secrets (Settings → Secrets): - CODEBERG_SECRET — personal access token with write access to the repo - ZOTERO_API_KEY — only needed if the Zotero endpoint becomes authenticated


Day-to-day operations

Publish a new blog post (no code)

  1. Create posts/YYYY-MM-DD-title/index.qmd
  2. Commit and push to main
  3. CI renders and deploys automatically (~2 min)

Publish a new blog post (with R/Python code chunks)

  1. Create posts/YYYY-MM-DD-title/index.qmd

  2. Render locally to freeze outputs:

    quarto render posts/YYYY-MM-DD-title/index.qmd
  3. Commit both the .qmd and _freeze/posts/YYYY-MM-DD-title/

  4. Push to main — CI uses the frozen outputs, does not re-execute code

Preview locally before pushing

quarto preview           # live-reload dev server at localhost:4200

Render the full site locally

quarto render

Zotero cache

How it works

scripts/fetch-zotero.js fetches all items from the Zotero “My Publications” public API and writes them to data/zotero-cache.json. It uses If-Modified-Since-Version to skip unchanged libraries (304 response). Pagination is automatic via the Total-Results response header.

The cache is consumed client-side by assets/js/zotero.js on publications.qmd.

Automatic refresh

The scheduled workflow (.forgejo/workflows/scheduled.yml) runs daily at 03:00 UTC and commits an updated cache to main only if the Zotero library changed (no-op otherwise). This triggers the publish workflow, redeploying the site.

Manual refresh

Option A — Forgejo UI: Go to Actions → “Refresh Zotero cache” → Run workflow.

Option B — local:

node scripts/fetch-zotero.js
git add data/zotero-cache.json
git commit -m "Update Zotero cache"
git push origin main

Troubleshooting the cache

Symptom Check
Publications page blank Open browser console; check /data/zotero-cache.json loads (200)
Cache not updating Check scheduled workflow logs in Forgejo Actions
API returns 429 Zotero rate limit; the public endpoint allows ~100 req/min
Stale items in cache Confirm items are in Zotero “My Publications” collection, then force refresh

CI workflows

publish.yml — triggered on push to main

  1. Checkout source
  2. Install Quarto 1.8.27
  3. quarto render (uses frozen outputs for posts/talks; runs scripts/fetch-zotero.js as pre-render hook)
  4. Force-push _site/ to pages branch

To update the Quarto version: edit the download URL and version string in .forgejo/workflows/publish.yml.

scheduled.yml — daily Zotero cache refresh

Runs at 03:00 UTC. Can also be triggered manually via workflow_dispatch from the Forgejo Actions UI.


Computational freeze

Posts and talks use freeze: true (set in posts/_metadata.yml and talks/_metadata.yml). Frozen HTML outputs live in _freeze/. CI never executes R or Python.

To re-execute a single post:

rm -rf _freeze/posts/YYYY-MM-DD-title
quarto render posts/YYYY-MM-DD-title/index.qmd
# commit the new _freeze/ outputs

To re-execute everything: rm -rf _freeze/ && quarto render


Adding a talk

  1. Create talks/YYYY-MM-DD-title/index.qmd (talk landing page)
  2. Optionally add talks/YYYY-MM-DD-title/slides.qmd (Revealjs)
  3. Add an entry to talks/talks_YYYY.yml (create the file if it’s a new year)
  4. Commit and push

Secrets rotation

  1. Generate a new Codeberg personal access token with repo write scope
  2. Update CODEBERG_SECRET in Forgejo repo Settings → Secrets
  3. Verify by triggering a manual workflow run

Recovering from a broken pages branch

The pages branch is always reconstructed by CI from _site/. If it becomes corrupt:

  1. Delete the pages branch on Codeberg
  2. Push any commit to main to trigger CI
  3. CI will create a fresh pages branch

File reference

Path Purpose
_quarto.yml Site config, navbar, theme, pre-render hook
posts/_metadata.yml Shared frontmatter for all posts (freeze, banner, TOC)
talks/_metadata.yml Shared frontmatter for all talks
scripts/fetch-zotero.js Zotero API fetch → data/zotero-cache.json
assets/js/zotero.js Client-side Zotero cache renderer
data/zotero-cache.json Cached Zotero API response (committed to repo)
.forgejo/workflows/publish.yml CI: render + deploy on push to main
.forgejo/workflows/scheduled.yml CI: daily Zotero cache refresh
_freeze/ Frozen computation outputs (committed)
_site/ Built site (not committed; lives only on pages branch)