@henriburau/lapdog (0.1.0)
Installation
@henriburau:registry=https://gitea.henriburau.de/api/packages/ace966/npm/npm install @henriburau/lapdog@0.1.0"@henriburau/lapdog": "0.1.0"About this package
lapdog
Markdown labs for the browser and the printed page. A TypeScript CLI and Vue 3 frontend for running hands-on seminars from a directory of Markdown files.
- Right-side navigation with frontmatter lab titles and nested task links.
- Every level-one heading (
#) becomes a task; each file can contain several tasks. - Live reload on file edits, additions, renames, removals, and local image changes.
- One print-ready workbook containing every lab, with a linked table of contents upfront.
- Headless PDF export, syntax highlighting, tables, relative images, and links between labs.
- Responsive layout, keyboard navigation, and locally bundled fonts for offline use.
- English and German interface with a persistent language selector, powered by vue-i18n.
Quick start
Private npm package
Requires Node.js 22.12+. Configure the Gitea registry and authenticate with a personal access token with package read permission:
npm config set @henriburau:registry https://gitea.henriburau.de/api/packages/ace966/npm/
npm config set -- '//gitea.henriburau.de/api/packages/ace966/npm/:_authToken' "$GITEA_TOKEN"
npm install --global @henriburau/lapdog
lapdog serve ./my-labs --title "My seminar"
Open http://127.0.0.1:4173. The package includes the built frontend, fonts, CLI, and sample labs; no build step is needed after installation. The npm scope is @henriburau, while the Gitea package owner is ace966, matching the existing Gitea projects.
For CLI PDF export, install the Chromium version used by the installed package once:
node "$(npm root -g)/@henriburau/lapdog/node_modules/playwright/cli.js" install --with-deps chromium
lapdog pdf ./my-labs --output ./workbook.pdf
Docker
The image includes Node.js, the built app, fonts, Chromium, and its Linux dependencies. It runs as the unprivileged node user, serves /labs on 0.0.0.0:4173, and supports both AMD64 and ARM64.
docker login gitea.henriburau.de --username ace966
docker run --rm --init \
-p 4173:4173 \
--mount "type=bind,source=$(pwd)/my-labs,target=/labs,readonly" \
gitea.henriburau.de/ace966/lapdog:latest
Use your package-read token as the registry password. Open http://localhost:4173. The mounted directory must exist and be readable by the container user. Edits to the host files are live-reloaded even with a read-only mount. Without a mount, the image serves the bundled sample seminar.
To set a title, append serve /labs --host 0.0.0.0 --title "My seminar" to the image name. Arguments replace the default Docker command, so include serve, the directory, and the bind address. On filesystems where Docker does not forward file events, add -e CHOKIDAR_USEPOLLING=true.
Export a PDF without installing anything on the host other than Docker:
mkdir -p output
docker run --rm --init \
--user "$(id -u):$(id -g)" \
--mount "type=bind,source=$(pwd)/my-labs,target=/labs,readonly" \
--mount "type=bind,source=$(pwd)/output,target=/output" \
gitea.henriburau.de/ace966/lapdog:latest \
pdf /labs --title "My seminar" --output /output/workbook.pdf
The output directory is writable by your host UID/GID; the bundled browser is accessible to that user. Image tags are latest for stable releases, the exact package version (for example, 0.1.0), edge for main, and sha-<full-commit-sha> for each published build.
From source
Requires Node.js 22.12+ and pnpm 10.18.0 (pinned in package.json).
pnpm install
pnpm run build
pnpm start ./examples --title "JavaScript workshop"
Open http://127.0.0.1:4173. Replace ./examples with your Markdown directory (relative or absolute). Markdown live reload works in both built and development modes.
For frontend development, run pnpm run dev. This starts the example seminar with Vite hot module replacement as well as Markdown live reload. For a different directory:
pnpm exec tsx src/server/cli.ts serve ./my-labs --dev
To use the lapdog command locally after building:
pnpm link --global
lapdog serve ./my-labs --title "My seminar"
lapdog serve ./my-labs --host 0.0.0.0 --port 8080
Interface language
Choose English or Deutsch from the language selector in the header. On the first visit,
lapdog uses the first supported language in your browser preferences (including regional variants
such as de-DE), falling back to English. Your selection is saved in this browser for future visits.
Navigation, status messages, accessibility labels, task counts, and the printed table of contents follow the selected language. Browser printing uses the current selection. Seminar titles, Markdown content, frontmatter values, and server diagnostic details are displayed as authored.
Translations live in src/client/locales/en.ts and src/client/locales/de.ts; shared vue-i18n setup
and language preference handling live in src/client/i18n.ts.
Write a lab
---
title: Working with data
description: Explore arrays and transformations.
order: 2
duration: 30 minutes
---
An optional introduction to this lab.
# Build a collection
Your first task goes here.
## A helpful hint
Subheadings stay inside the task.
# Find what matters
Your second task goes here.
Frontmatter
| Field | Required | Meaning |
|---|---|---|
title |
Yes | Non-empty string used in navigation, lab headings, and the table of contents |
description |
No | Short string shown below the lab title |
order |
No | Number controlling lab order; lower numbers come first |
duration |
No | Display string, such as 30 minutes |
Files are discovered recursively, including .MD. Hidden paths and node_modules are skipped, and discovery does not follow symlinks. Labs with an order come first; ties and labs without an order use natural filename ordering (2 before 10). Every discovered Markdown file must have a title. Invalid files produce a useful error in the browser; the last valid workbook stays visible until you fix the file.
Markdown is parsed with markdown-it, frontmatter with gray-matter / js-yaml, and fenced code blocks with highlight.js. Raw HTML is escaped. Supported Markdown includes headings, lists, quotes, code, tables, links, and images. A fenced # comment is not a task. Standard Markdown level-one setext headings also count as tasks.
Links and assets
Resolve paths relative to the Markdown file; leading / paths are relative to the lab directory:

[Next lab](./02-next-lab.md)
[A task in another lab](./02-next-lab.md#build-a-collection)
[A heading in this lab](#a-helpful-hint)
Heading fragments are lowercased, punctuation is removed, and spaces become hyphens. Duplicate headings get suffixes (-2, -3). Each lab has its own anchor namespace. Common image, media, font, PDF, text, and CSV assets are served from the selected directory. Assets cannot resolve outside that directory.
Print and PDF
From the browser
Click Print / PDF (or use your browser’s print command), then choose Save as PDF. The print stylesheet includes all labs, regardless of the selected lab or navigation filter, puts the table of contents first, and starts each lab on a new page. Navigation and controls are hidden. For the cleanest output, turn off the browser’s default headers and footers.
From the command line
Install Playwright’s Chromium once, then export:
pnpm exec playwright install chromium
pnpm run pdf ./examples --title "JavaScript workshop" --output ./workbook.pdf
# Or, after pnpm link --global:
lapdog pdf ./my-labs --title "My seminar" --output ./workbook.pdf
When running from source, build the frontend with pnpm run build before exporting. On Linux systems missing browser libraries, use pnpm exec playwright install --with-deps chromium. The published package already contains the frontend, and the Docker image also contains Chromium and its libraries.
Export starts a temporary loopback server, waits for the Vue app, fonts, and images, writes an A4 PDF, and shuts down. The PDF contains a linked lab/task table of contents, document outline, and page-number footers. The table of contents provides links, rather than calculated page numbers. Fonts are bundled locally; external images still need a network connection. Missing images fail CLI export rather than silently producing an incomplete workbook.
Commands
lapdog serve [directory] --title <title> --port <port> --host <host> [--dev]
lapdog pdf [directory] --title <title> --output <file>
Defaults: directory examples relative to your current working directory, title Seminar labs, host 127.0.0.1, port 4173, output lapdog.pdf.
Development and verification
pnpm run dev # Vue HMR + Markdown live reload
pnpm run build # Type-check Vue/TypeScript and build frontend + CLI
pnpm test # Parser, ordering, links, validation, and discovery tests
pnpm run test:e2e # Browser, hot reload, print, and real PDF export (build + Chromium required)
pnpm run test:package # Install a packed release in isolation and exercise its CLI and frontend (build required)
pnpm run test:docker # Test lapdog:test with read-only mounted labs and real PDF export
pnpm run lint # Lint TypeScript and Vue scripts with Oxlint
pnpm run lint:fix # Apply automatic lint fixes
pnpm run format # Format the source with Oxfmt
pnpm run format:check # Check formatting without writing changes
Oxc provides linting through Oxlint and formatting through Oxfmt. Configuration lives in .oxlintrc.json and .oxfmtrc.json. For reproducible installs, use pnpm install --frozen-lockfile.
Build and test the Docker image locally:
docker build -t lapdog:test .
pnpm run test:docker
Gitea publishing
.gitea/workflows/publish.yml follows the instance settings in ../gitlab-for-developer: gitea.henriburau.de, the ace966 owner, the ubuntu runner label, and the REGISTRY_TOKEN secret.
Instance and repository setup
- Host this repository in Gitea and enable Repository Settings → Actions and the package registry.
- Use an
ubuntu-labelled runner with Node action support and access to a Docker daemon. Docker Buildx and QEMU are set up by the workflow; the daemon must allow the privileged binfmt setup for cross-platform builds. The verification job also needs permission to install Playwright's OS libraries. - Add the repository Actions secret
REGISTRY_TOKEN, a personal access token with package read/write permission for the publishing account. As in the reference workflow, Docker login usesgitea.actor; the release pusher and token owner should be the same account (ace966). - Make the package owner private in Gitea before publishing private packages. Gitea package read access follows the user/organization visibility, not the linked repository's visibility. For the configured user namespace, set
ace966's visibility to private; only that user can read its private packages. If packages need to be shared with a team, use a private organization and update the owner in.npmrc,package.json'spublishConfig, the workflow, and the install/image URLs. Linking a package to a private repository alone does not make it private.publishConfig.access: restricteddoes not replace Gitea's owner access rules.
The package's publishConfig.registry directs publishing to Gitea. Authentication comes from the Actions secret via the temporary npm configuration created by setup-node.
Pipeline and releases
- Pull requests: lint, formatting, unit tests, build, browser/PDF tests, isolated npm package installation, and a native Docker build with a read-only-volume/PDF smoke test.
- Pushes to
main: the same checks, then publish multi-platform Docker tagsedgeandsha-<commit>. - Version tags (
v*): the same checks, then publish the versioned Docker image and npm package. The tag must matchpackage.jsonexactly, for examplev0.1.0for version0.1.0. Stable releases update Docker/npmlatest; prereleases such asv0.2.0-beta.1use their versioned Docker tag and npmnext.
To release, update package.json's version, commit it, and push a matching tag:
git add package.json
git commit -m "Release 0.1.0"
git tag v0.1.0
git push origin main
git push origin v0.1.0
The CLI reads its version from package.json. npm's prepack hook builds the release before upload. Gitea rejects duplicate npm versions, so increment the version for each new release. After the first publication, the packages can be linked to the lapdog repository from their Gitea package settings.
Structure
src/server/ TypeScript CLI, Markdown pipeline, Express/SSE server, PDF export
src/client/ Vue application shell and shared base styles
components/ Focused UI components with colocated screen/print styles
composables/ Seminar loading/live updates and hash-based lab navigation
styles/ Styles for server-rendered Markdown
src/shared/ Shared lab, task, and seminar types
examples/ A three-lab sample seminar
tests/ Parser and end-to-end checks
The built server serves the Vue frontend and watches the selected directory with Chokidar. Server-sent events trigger content refreshes without a full-page reload. Vite is only needed for development and builds; PDF export uses Playwright’s Chromium and the same Vue rendering and print CSS as the browser.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @fontsource-variable/dm-sans | ^5.2.6 |
| @fontsource-variable/manrope | ^5.2.6 |
| chokidar | ^4.0.3 |
| commander | ^14.0.0 |
| express | ^5.1.0 |
| gray-matter | ^4.0.3 |
| highlight.js | ^11.11.1 |
| js-yaml | ^4.1.0 |
| markdown-it | ^14.1.0 |
| playwright | ^1.55.0 |
| vue | ^3.5.21 |
| vue-i18n | ^11.4.12 |
Development Dependencies
| ID | Version |
|---|---|
| @types/express | ^5.0.3 |
| @types/js-yaml | ^4.0.9 |
| @types/markdown-it | ^14.1.2 |
| @types/node | ^24.3.0 |
| @vitejs/plugin-vue | ^6.0.1 |
| oxfmt | ^0.70.0 |
| oxlint | ^1.85.0 |
| tsx | ^4.20.5 |
| typescript | ^5.9.2 |
| vite | ^7.1.3 |
| vue-tsc | ^3.0.6 |