A live, offline-first lab handbook for hands-on workshops. Authors write the workshop as a folder of Markdown files; participants follow along step by step in a browser. Code blocks have copy buttons, diffs between steps are pretty-printed, Mermaid diagrams render, and progress is tracked in each participant's browser.
It runs on the participant's own device — an ARM64 SBC such as the Arduino Uno Q or Arduino Ventuno Q — and applying a step's solution writes straight to the Arduino app on that same device (under /home/arduino/ArduinoApps), so a click in the browser changes the code the participant is building.
Content is read from disk on every request, so an author can fix a typo mid-workshop and have the change show up on refresh.
📖 Authoring guide → DOCS.md — the full reference for the
content/ folder, frontmatter, side quests, attachments, patches, and the playbook for turning an event agenda into a workshop.
From the repository root:
go run . -content ./contentThe example Arduino-blink workshop in content/ will start serving. You'll see something like:
workshop.ino · serving /…/workshop.ino/content
http://localhost:8080
http://192.168.1.86:8080
Open http://localhost:8080 in a browser on the device. (The other URLs let you reach it from your laptop while authoring.)
go build -o workshop.ino .
./workshop.ino -content ./my-workshop -addr :8080| Flag | Default | What it does |
|---|---|---|
-addr |
:8080 |
Address to listen on (:9000, 127.0.0.1:8080, …). |
-content |
./content |
Path to your workshop folder (resolved against CWD). |
-apps |
~/ArduinoApps |
Directory holding the Arduino apps a step's solution is applied to. Resolves to $HOME/ArduinoApps (falling back to /home/arduino/ArduinoApps if $HOME can't be resolved). Point it at a scratch dir when testing on a dev machine. |
workshop.ino is meant to run on the participant's device (the SBC). Two ways to install it:
Applying a solution needs write access to the apps folder (
/home/arduino/ArduinoApps). The native binary has everything out of the box, whereas the sample Compose file bind-mounts that host folder into the container read-write.
-
Grab
workshop.ino-linux-arm64from the latest GitHub Release. -
Drop it in any directory with a
content/folder beside it:anywhere/ workshop.ino-linux-arm64 (the binary, rename if you like) content/ (your workshop contents) -
Run it:
chmod +x workshop.ino-linux-arm64 ./workshop.ino-linux-arm64
The default
-content ./contentfinds the folder next to it; the handbook is onhttp://<host-ip>:8080/.
For a managed service, point deploy/workshop.ino.service at wherever you dropped the binary (edit WorkingDirectory and ExecStart), then:
sudo cp deploy/workshop.ino.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now workshop.ino- Copy
deploy/docker/(the compose file andrun-docker-compose.sh) to the board. - Put a
content/folder next to them. ./run-docker-compose.sh— runs in the foreground; Ctrl+C stops the stack.
The handbook is then on http://<board-ip>:8080/. The Compose file mounts the host's /home/arduino/ArduinoApps read-write at the container's /apps, where the image is configured to apply solutions (-apps /apps). Applied files are owned by the uid:gid the container runs as; run-docker-compose.sh passes your HOST_UID/HOST_GID so you own them (defaulting to the device's arduino user, 1000:1000). The mounted apps dir must be writable by that uid.
For a managed service that survives reboots and restarts on failure, install deploy/docker/workshop.ino.service (edit its WorkingDirectory to where you dropped the files):
sudo cp deploy/docker/workshop.ino.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now workshop.inogo test ./...- main.go — CLI flags, LAN URL printing, server startup.
- internal/content/ — pure logic:
- render.go — YAML frontmatter parsing.
- scan.go — folder walking, ordering, side-quest discovery.
- markdown.go — Markdown rendering (goldmark) with chroma highlighting, Mermaid passthrough, relative-image rewriting.
- patch.go — unified-diff parsing into
DiffFile/DiffHunk/DiffLine.
- internal/server/ — HTTP layer:
- handlers.go — index, step, side-quest, download routes; view-model construction.
- download.go — path-traversal guard for
/dl/…. - templates/ — embedded
html/templatepages. - assets/ — embedded CSS, JS (Alpine, Mermaid), logo, favicon.
- content/ — the example Arduino-blink workshop (runs out of the box, exercises every feature).
- DOCS.md — authoring guide for workshop content.
Content under content/ is read from disk on every request, so edits to .md / .patch / asset files show up on refresh without restarting. The templates, CSS and JS are embedded into the binary via go:embed, so changes to those need a rebuild (go run . again, or go build).