···11+FROM ocaml/opam:debian-ocaml-5.2 AS build
22+33+# System dependencies
44+RUN sudo apt-get update && sudo apt-get install -y \
55+ autoconf \
66+ pkg-config \
77+ libgmp-dev \
88+ libev-dev \
99+ libffi-dev \
1010+ libssl-dev \
1111+ python3 \
1212+ jq \
1313+ && sudo rm -rf /var/lib/apt/lists/*
1414+1515+# Set up oxcaml switch with the ox opam repo
1616+RUN opam update --all \
1717+ && opam switch create 5.2.0+ox \
1818+ --repos ox=git+https://github.com/oxcaml/opam-repository.git,alpha=git+https://github.com/kit-ty-kate/opam-alpha-repository.git,default \
1919+ && eval $(opam env --switch 5.2.0+ox)
2020+2121+# Copy opam file first for dependency caching
2222+WORKDIR /home/opam/mono
2323+COPY --chown=opam:opam root.opam .
2424+RUN opam install --switch 5.2.0+ox --deps-only -y ./root.opam
2525+2626+# Copy the full monorepo source
2727+COPY --chown=opam:opam . .
2828+2929+# Build and install plugins, then build the site (without universes)
3030+RUN eval $(opam env --switch 5.2.0+ox) \
3131+ && dune build @install \
3232+ && dune install 2>/dev/null \
3333+ && dune build @site \
3434+ && dune build @doc
3535+3636+# Assemble the site into _site/
3737+RUN mkdir -p _site \
3838+ && cp -rf _build/default/site/_html/* _site/ \
3939+ && cp -rf _build/default/_doc/_html/reference/* _site/reference/ \
4040+ && mkdir -p _site/odoc.support/ \
4141+ && cp -rf _build/default/_doc/_html/odoc.support/* _site/odoc.support/
4242+4343+# Universe building (jtw is built from source in js_top_worker/)
4444+RUN eval $(opam env --switch 5.2.0+ox) \
4545+ && jtw opam astring base brr note mime_printer fpath rresult \
4646+ opam-format bos odoc.model tyxml yojson uri jsonm \
4747+ js_top_worker-widget-leaflet \
4848+ tessera-geotessera-jsoo tessera-viz-jsoo \
4949+ onnxrt -o _site/_opam
5050+5151+# Deploy onnxrt assets
5252+RUN if [ -f onnxrt/example/sentiment/model_quantized.onnx ]; then \
5353+ cp onnxrt/example/sentiment/vocab.txt _site/_opam/vocab.txt \
5454+ && cp onnxrt/example/sentiment/model_quantized.onnx _site/_opam/model_quantized.onnx; \
5555+ fi \
5656+ && cp onnxrt/example/add.onnx _site/_opam/add.onnx
5757+5858+# Lightweight final image with just the built site
5959+FROM nginx:alpine
6060+COPY --from=build /home/opam/mono/_site /usr/share/nginx/html
6161+EXPOSE 80
+148
build-site.sh
···11+#!/bin/bash
22+# Build the full jon.recoil.org site into _site/.
33+#
44+# Dune outputs go into _build/ (which dune controls and may wipe).
55+# We assemble the final site into _site/ so that expensive artifacts
66+# like universes persist across rebuilds.
77+#
88+# Usage:
99+# ./build-site.sh # build everything
1010+# ./build-site.sh --fresh # rebuild universes from scratch
1111+# ./build-site.sh --serve # build and serve on port 8080
1212+1313+set -euo pipefail
1414+1515+MONO=$(cd "$(dirname "$0")" && pwd)
1616+SITE="$MONO/_site"
1717+DUNE_SITE="$MONO/_build/default/site/_html"
1818+DUNE_DOC="$MONO/_build/default/_doc/_html"
1919+SERVE=false
2020+FRESH=false
2121+2222+for arg in "$@"; do
2323+ case "$arg" in
2424+ --serve) SERVE=true ;;
2525+ --fresh) FRESH=true ;;
2626+ esac
2727+done
2828+2929+if $FRESH; then
3030+ echo "=== --fresh: removing _site ==="
3131+ rm -rf "$SITE"
3232+fi
3333+3434+mkdir -p "$SITE"
3535+3636+# Ensure we're on the right switch.
3737+export OPAMSWITCH=5.2.0+ox
3838+eval "$(opam env)"
3939+4040+echo "=== Step 1: Build and register plugins ==="
4141+cd "$MONO"
4242+dune build @install
4343+dune install 2>/dev/null
4444+echo " plugins registered"
4545+4646+echo ""
4747+echo "=== Step 2: Build site content ==="
4848+dune build @site 2>&1 | grep -v '^Warning\|^File\|^$' | tail -5 || true
4949+echo " dune @site done"
5050+5151+echo ""
5252+echo "=== Step 3: Build reference docs ==="
5353+dune build @doc 2>&1 | tail -5 || true
5454+echo " dune @doc done"
5555+5656+echo ""
5757+echo "=== Step 4: Assemble site ==="
5858+cp -rf "$DUNE_SITE/"* "$SITE/"
5959+cp -rf "$DUNE_DOC/reference/"* "$SITE/reference/"
6060+mkdir -p "$SITE/odoc.support/"
6161+cp -rf "$DUNE_DOC/odoc.support/"* "$SITE/odoc.support/"
6262+echo " assembled into $SITE/"
6363+6464+echo ""
6565+echo "=== Step 5: Build site universe (notebooks) ==="
6666+if [ -f "$SITE/_opam/worker.js" ]; then
6767+ echo " universe already exists, skipping (use --fresh to rebuild)"
6868+else
6969+ jtw opam astring base brr note mime_printer fpath rresult \
7070+ opam-format bos odoc.model tyxml yojson uri jsonm \
7171+ js_top_worker-widget-leaflet \
7272+ tessera-geotessera-jsoo tessera-viz-jsoo \
7373+ onnxrt -o "$SITE/_opam"
7474+ echo " universe built → $SITE/_opam/"
7575+fi
7676+7777+echo ""
7878+echo "=== Step 6: Deploy onnxrt example assets ==="
7979+SENTIMENT_SRC="$MONO/onnxrt/example/sentiment"
8080+if [ ! -f "$SENTIMENT_SRC/model_quantized.onnx" ]; then
8181+ echo " downloading DistilBERT model..."
8282+ bash "$SENTIMENT_SRC/download_model.sh"
8383+fi
8484+cp "$SENTIMENT_SRC/vocab.txt" "$SITE/_opam/vocab.txt"
8585+cp "$SENTIMENT_SRC/model_quantized.onnx" "$SITE/_opam/model_quantized.onnx"
8686+cp "$MONO/onnxrt/example/add.onnx" "$SITE/_opam/add.onnx"
8787+echo " deployed vocab.txt + model_quantized.onnx + add.onnx → $SITE/_opam/"
8888+8989+echo ""
9090+echo "=== Step 7: Build demo universes ==="
9191+DEMO_DIR="$SITE/reference/odoc-interactive-extension"
9292+9393+if [ -f "$DEMO_DIR/universe/worker.js" ]; then
9494+ echo " demo universes already exist, skipping (use --fresh to rebuild)"
9595+else
9696+ UNIVERSES=$(mktemp -d)
9797+ trap 'rm -rf "$UNIVERSES"' EXIT
9898+9999+ echo " building default universe (cmdliner, 5.2.0+ox switch)..."
100100+ jtw opam --switch=5.2.0+ox -o "$UNIVERSES/default" cmdliner
101101+102102+ echo " building v3 universe (cmdliner, 5.2.0+ox switch)..."
103103+ jtw opam --switch=5.2.0+ox -o "$UNIVERSES/v3" cmdliner
104104+105105+ echo " building oxcaml universe (5.2.0+ox switch)..."
106106+ jtw opam --switch=5.2.0+ox -o "$UNIVERSES/oxcaml"
107107+108108+ for d in universe universe-v2 universe-v3 universe-oxcaml; do
109109+ rm -rf "$DEMO_DIR/$d"
110110+ done
111111+112112+ cp -r "$UNIVERSES/default" "$DEMO_DIR/universe"
113113+ echo " deployed universe/"
114114+115115+ cp -r "$UNIVERSES/default" "$DEMO_DIR/universe-v2"
116116+ echo " deployed universe-v2/"
117117+118118+ cp -r "$UNIVERSES/v3" "$DEMO_DIR/universe-v3"
119119+ echo " deployed universe-v3/"
120120+121121+ cp -r "$UNIVERSES/oxcaml" "$DEMO_DIR/universe-oxcaml"
122122+ echo " deployed universe-oxcaml/"
123123+124124+ for d in universe universe-v2 universe-v3 universe-oxcaml; do
125125+ cp "$SITE/_x-ocaml/x-ocaml.js" "$DEMO_DIR/$d/x-ocaml.js"
126126+ done
127127+fi
128128+129129+echo ""
130130+echo "=== Done ==="
131131+echo ""
132132+echo "Site root: $SITE/"
133133+echo ""
134134+echo "Key pages:"
135135+echo " /index.html — site home"
136136+echo " /blog/index.html — blog index"
137137+echo " /notebooks/index.html — notebooks index"
138138+echo " /notebooks/foundations/index.html — foundations of CS"
139139+echo " /projects/index.html — projects"
140140+echo " /reference/ — API reference docs"
141141+echo " /reference/odoc-interactive-extension/ — interactive demos"
142142+143143+if $SERVE; then
144144+ echo ""
145145+ echo "Starting HTTP server on http://localhost:8080"
146146+ cd "$SITE"
147147+ exec python3 -m http.server 8080
148148+fi
+28
deploy-live.sh
···11+#!/bin/bash
22+# Deploy the site to jon.recoil.org (live/production server).
33+#
44+# Usage:
55+# ./deploy-live.sh # rsync _site/ to live server
66+# ./deploy-live.sh --build # build first, then deploy
77+88+set -euo pipefail
99+1010+MONO=$(cd "$(dirname "$0")" && pwd)
1111+SITE="$MONO/_site"
1212+TARGET="jon.recoil.org"
1313+DEST="/var/www/jon.recoil.org/"
1414+1515+for arg in "$@"; do
1616+ case "$arg" in
1717+ --build) "$MONO/build-site.sh" ;;
1818+ esac
1919+done
2020+2121+if [ ! -d "$SITE" ]; then
2222+ echo "Error: $SITE does not exist. Run ./build-site.sh first."
2323+ exit 1
2424+fi
2525+2626+echo "=== Deploying to $TARGET ==="
2727+rsync -avz --delete "$SITE/" "$TARGET:$DEST"
2828+echo "=== Deployed to https://$TARGET/ ==="
+28
deploy-test.sh
···11+#!/bin/bash
22+# Deploy the site to jon-test.ludl.am (test server).
33+#
44+# Usage:
55+# ./deploy-test.sh # rsync _site/ to test server
66+# ./deploy-test.sh --build # build first, then deploy
77+88+set -euo pipefail
99+1010+MONO=$(cd "$(dirname "$0")" && pwd)
1111+SITE="$MONO/_site"
1212+TARGET="jon-test.ludl.am"
1313+DEST="/var/www/jon-test.ludl.am/"
1414+1515+for arg in "$@"; do
1616+ case "$arg" in
1717+ --build) "$MONO/build-site.sh" ;;
1818+ esac
1919+done
2020+2121+if [ ! -d "$SITE" ]; then
2222+ echo "Error: $SITE does not exist. Run ./build-site.sh first."
2323+ exit 1
2424+fi
2525+2626+echo "=== Deploying to $TARGET ==="
2727+rsync -avz --delete "$SITE/" "$TARGET:$DEST"
2828+echo "=== Deployed to https://$TARGET/ ==="
+4-1
odoc-docsite/doc/index.mld
···11-{0 odoc documentation site shell}
11+{0 Odoc Documentation-site Shell}
22+33+This plugin for odoc provides a more modern styling for odoc's output, including
44+SPA-style navigation.
+3
odoc-interactive-extension/doc/index.mld
···11{0 Interactive OCaml Extension for odoc}
22+33+See the various notebooks in the sidebar for examples of this extension.
44+
+6-1
odoc/doc/odoc-parser/index.mld
···11-{0 odoc comment parser}
11+{0 The [odoc] comment parser}
22+33+This is the parser for odoc-formatted comments and mld files.
44+55+For API documentation see {!Odoc_parser}.
66+
+5
odoc/doc/sherlodoc/index.mld
···11{0 Sherlodoc search engine}
22+33+This is the search engine used to provide client and server side search.
44+55+For a full deployment, see {:https://doc.sherlocode.com/}
66+
···2121- I was interested in whether we'll be able to do inference in reasonable time using these
2222 notebooks. {{:https://onnx.ai/}ONNX} has a web version of its runtime, so I got Claude to
2323 make some bindings, and checked it was working by doing a sentiment analysis notebook. This
2424- is working nicely, so the next step is to do something a bit more useful.
2424+ is working nicely, so the next step is to do something a bit more useful. Try it
2525+ {{:/reference/onnxrt/sentiment_example.html}here}.
2526- The docs CI was again causing problems. This time it had decided that it had never built
2627 anything, and therefore needed to rebuilt the entire world. However, despite being set up
2728 as a custom dedicated runner, all its jobs were queued waiting to start. It turned out that