diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7b87a764..50716f0a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -68,3 +68,95 @@ jobs:
run: |
nix shell nixpkgs#trealla --command tpl prolog-sessions/kb.pl \
-g "(kb_consistent -> halt(0) ; (kb_report, halt(1)))"
+
+ # ---------------------------------------------------------------------------
+ # GitHub Pages delivery (improvise-d31).
+ #
+ # Two jobs on purpose. `pages-build` runs on every push and pull request, so
+ # a change that breaks `web/build.sh` or the bundle fails on the PR that made
+ # it — the build is a gate. `pages-deploy` runs only on a push to main and
+ # `needs` the two check jobs above, so nothing that failed `cargo test` or the
+ # wasm32 gate can reach the published site. The deploy depends on those checks
+ # rather than repeating them (improvise-d31 AC 2).
+ #
+ # Not path-filtered, deliberately. The bundle is `improvise-wasm-client` plus
+ # improvise-protocol plus improvise-core plus improvise-formula, so a filter
+ # that meant "skip when the bundle cannot have changed" would have to name
+ # most of the workspace, and the failure mode of getting it wrong is a stale
+ # site with no signal that it is stale. A redundant build is the cheaper error.
+ pages-build:
+ name: Browser bundle + static site
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: wasm32-unknown-unknown
+ - uses: Swatinem/rust-cache@v2
+ # Pinned release binary rather than `cargo install` (a three-minute
+ # source build) or a third-party action. wasm-pack runs its own bundled
+ # `wasm-opt`, which is why improvise-wasm-client's Cargo.toml names
+ # `--enable-bulk-memory`: without it the optimizer rejects the
+ # `memory.copy` LLVM emits regardless.
+ # v0.15.0 is what `nix develop` supplies, and the bundle was built and
+ # measured against it locally (346 KB, 120 KB gzipped). Pinning the same
+ # version is what makes that local run evidence about this one.
+ - name: Install wasm-pack
+ env:
+ WASM_PACK_VERSION: v0.15.0
+ run: |
+ set -eu
+ url="https://github.com/rustwasm/wasm-pack/releases/download/${WASM_PACK_VERSION}/wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl.tar.gz"
+ curl -sSfL "$url" | tar -xz --strip-components=1 -C /usr/local/bin \
+ "wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl/wasm-pack"
+ wasm-pack --version
+
+ # The same script a developer runs locally, so there is one answer to
+ # "how is the bundle built" rather than two that can drift.
+ - name: Build the wasm bundle
+ run: ./web/build.sh
+
+ # `web/` is the source directory: it also holds build.sh and .gitignore,
+ # which have no business being served. Assemble what ships.
+ - name: Assemble the site
+ run: |
+ set -eu
+ mkdir -p site
+ cp web/index.html web/app.js web/style.css site/
+ cp -rf web/pkg site/pkg
+ rm -f site/pkg/.gitignore site/pkg/package.json
+ find site -type f | sort
+ du -sh site
+
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: site
+
+ pages-deploy:
+ name: Deploy to GitHub Pages
+ needs: [test, wasm, pages-build]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ # A superseded deploy is allowed to finish rather than being cancelled:
+ # `deploy-pages` cancelled midway can leave the Pages deployment in a
+ # half-applied state, and the next push republishes anyway.
+ concurrency:
+ group: pages
+ cancel-in-progress: false
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ # `enablement: true` asks the API to turn Pages on with source =
+ # GitHub Actions. It succeeds only if the repository allows the workflow
+ # token to do so; if it does not, this step is where the run fails and
+ # the fix is the repository setting, not this file (improvise-3gy).
+ - uses: actions/configure-pages@v5
+ with:
+ enablement: true
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/README.md b/README.md
index 69810dc5..ae669a87 100644
--- a/README.md
+++ b/README.md
@@ -176,6 +176,35 @@ improvise cmd 'add-cat Region' 'add-item Region East' -f model.improv
improvise script setup.txt -f model.improv
```
+## In the browser
+
+The browser page is at **
+ improvise in the browser is a thin client: the document lives in a
+ server. Run improvise-ws-server model.improv --port 8787
+ and reload with ?ws=ws://127.0.0.1:8787. Served over
+ https, a ws:// address is blocked as mixed content — use
+ wss:// or a local http server. Running with no separate
+ process at all is the in-page worker (improvise-9x6), and it is not
+ built yet.
+