# wiki-print Export a [Wiki.js](https://js.wiki/) page and every page below it into one PDF. The exporter talks to the Wiki.js GraphQL API, collects the page subtree, renders all pages into a single printable HTML document, and prints it with Chromium. It is packaged as a small web app that runs in a Podman pod. ## What you get - One PDF containing the starting page and every page whose path is below it (`docs` → `docs`, `docs/install`, `docs/install/linux`, …). - **Working links:** links between exported pages become internal PDF links. Links to pages outside the exported subtree and external websites stay clickable online URLs. - The table of contents, heading anchors and same-page `#anchor` links keep working. - Images are fetched from the wiki and embedded as data URIs, so the PDF is self-contained. - A cover page, table of contents, page numbers and PDF bookmarks (from the page headings). - A simple web form that lists every page from the wiki so you can pick a starting page; the wiki URL and API key are configured on the server. - Optional unpublished-page export and image embedding. ## Quick start Requirements: Podman 4+. Node.js is only needed for local development. ```bash export WIKI_URL='https://wiki.example.com' export WIKI_API_KEY='your-wiki-js-api-key' ./scripts/up.sh ``` `up.sh` stores both values in the Podman secret `wiki-print-config`, passes them to the container as `WIKI_URL` and `WIKI_API_KEY`, builds `localhost/wiki-print:latest` on first run, and creates the pod `wiki-print` with the web UI on [http://localhost:8080](http://localhost:8080). On macOS the script starts the Podman machine if it is not running. Then open the UI and choose a **starting page** from the list of pages loaded from your wiki. The list can be filtered by title or path, and unpublished pages are hidden unless **Include unpublished pages** is enabled. Press **Create PDF**. The export runs in the background and the download link appears when it is finished. Useful commands: ```bash ./scripts/build.sh # rebuild the image ./scripts/up.sh # create/recreate the pod (reads WIKI_URL and WIKI_API_KEY) ./scripts/logs.sh # follow the app logs ./scripts/down.sh # remove the pod and the configuration secret ./scripts/test.sh # build the test image and run all tests ``` ## API key In Wiki.js go to **Administration → API Access**, create a key and assign it to a group that can read the pages you want to export. The group needs the **read:pages** permission. Pass the key to the container as the environment variable `WIKI_API_KEY`: ```bash export WIKI_API_KEY='eyJhbGc...' ./scripts/up.sh ``` The web form does not ask for the key and the browser never receives it. `up.sh` keeps both the URL and the key in the Podman secret `wiki-print-config`; the pod manifest exposes them as `WIKI_URL` and `WIKI_API_KEY` inside the container. When either value changes, export the new value and run `./scripts/up.sh` again. `read:source` is optional. It is only used as a fallback for pages whose rendered HTML is missing: the exporter can then fetch the markdown source and render it itself. Without it, such a page is included as a link-only page and a warning is shown. The key is used only for server-side Wiki.js requests and for fetching images from the wiki. It is never sent to the browser or included in job status responses, PDFs, or log output. ## How the subtree is selected The UI loads the page list from the Wiki.js GraphQL API. Each entry carries its path and locale, so selecting a page also selects the right locale even when the same path exists in several languages. Pages are grouped by locale in the list. After you pick a starting page, every page in the same locale whose path starts with `/` is included. Paths are compared on segment boundaries, so `docs` includes `docs/install` but not `docs-other`. Examples: | Starting page | Result | | --- | --- | | `docs` | `docs`, `docs/install`, `docs/install/linux`, … | | `docs/install` | `docs/install`, `docs/install/linux`, … | | `docs-other` | not part of the `docs` subtree | | `docs/draft` | only exported when **Include unpublished pages** is enabled | ## Link behaviour The renderer rewrites links before printing: - Link target is an exported page → `#internal-anchor` in the PDF. - Link target is a heading on an exported page → the matching prefixed heading anchor. - Link target is another wiki page outside the subtree → absolute wiki URL, still clickable. - Link target is an external website → unchanged, still clickable. - Link target is another locale → absolute wiki URL, still clickable. - `mailto:`, `tel:` and similar links are left alone. - Scripts, event handlers, forms and iframes are removed for safe printing; embedded content is replaced with a clickable link. Chromium's print-to-PDF produces URI actions for external links and GoTo actions for internal anchors. Most PDF readers support both. ## Podman networking A container cannot reach the host's `localhost`. Depending on where Wiki.js runs: | Wiki.js location | `WIKI_URL` to use | | --- | --- | | On the same Mac/host | `http://host.containers.internal:3000` | | On the LAN | `http://192.168.1.50:3000` | | On a public server | `https://wiki.example.com` | | In another container | the container name, if both containers share a Podman network | Set `WIKI_URL` before running `./scripts/up.sh`. If Wiki.js uses HTTPS with a self-signed certificate, the container's Chromium/Node trust store must trust it. For a quick local setup, use the plain HTTP URL of the wiki. The pod publishes port `8080` on the host. To change it, edit `podman/wiki-print.pod.yaml` and the health check port in the same file. ## Configuration The pod manifest sets the most important values. You can also pass environment variables to `podman play kube` by editing `podman/wiki-print.pod.yaml`: | Variable | Default | Meaning | | --- | --- | --- | | `WIKI_URL` | required | Root URL of the Wiki.js installation | | `WIKI_API_KEY` | required | Wiki.js API key used by the server for all exports | | `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` or `silent` | | `LOG_FORMAT` | `text` | `text` for humans, `json` for log collectors | | `PORT` | `8080` | HTTP port inside the container | | `MAX_PAGES` | `500` | Default subtree size limit (the form can lower it) | | `MAX_QUEUE` | `5` | Maximum number of waiting exports | | `EXPORT_CONCURRENCY` | `1` | Number of PDFs rendered at once | | `PAGE_FETCH_CONCURRENCY` | `4` | Parallel GraphQL page requests | | `JOB_TTL_MINUTES` | `120` | How long finished PDFs stay available | | `AUTH_USER` / `AUTH_PASSWORD` | unset | Optional HTTP Basic authentication for the UI | | `CHROMIUM_PATH` | `/usr/bin/chromium` | Chromium executable | Example with a password-protected UI: add `AUTH_USER` and `AUTH_PASSWORD` to the container's `env` list in the manifest and recreate the pod. ## Logging All logs go to stderr, which Podman captures. `./scripts/logs.sh` follows them. The default text format is one line per event: ```text 2026-11-12T11:16:18.549Z INFO server started app=wiki-print host=0.0.0.0 port=8080 maxPages=500 apiKeyConfigured=true 2026-11-12T11:16:19.195Z INFO request app=wiki-print requestId=ab9d6a31 method=POST path=/api/exports status=202 durationMs=4 2026-11-12T11:16:19.195Z INFO export queued app=wiki-print jobId=... startPage=docs locale=en queueDepth=1 2026-11-12T11:16:24.720Z INFO PDF rendered app=wiki-print jobId=... filename=docs-wiki-....pdf pageCount=5 durationMs=3120 2026-11-12T11:16:24.741Z INFO export finished app=wiki-print jobId=... durationMs=5546 pageCount=5 warningCount=1 ``` What is logged: - Server startup configuration, without the API key. - Every API request with method, path, status, duration and a request ID. The same ID is returned in the `X-Request-Id` response header. - Job lifecycle: queued, started, stage changes, warnings, completion and failures with a job ID. - Export timings for the page list, content fetching, HTML building and PDF rendering. - Wiki.js GraphQL requests and retries at `debug` level. - Image embedding counts at `debug` level. - Errors with stack traces in the logs; the browser only receives the message and error code. Set `LOG_LEVEL=debug` to include per-request Wiki.js timings and per-page fetch details. Set `LOG_FORMAT=json` to get one JSON object per line for Loki, Elastic, journald or similar collectors: ```json {"timestamp":"2026-11-12T11:16:19.195Z","level":"info","message":"export finished","app":"wiki-print","jobId":"...","pageCount":5} ``` Credential-like fields are redacted automatically, and the API key is never logged. The CLI keeps its live progress display and shows logs from `warn` upward by default; set `LOG_LEVEL=info` to see the full export log there too. ## CLI The same exporter is available without the web UI: ```bash export WIKI_URL=https://wiki.example.com export WIKI_API_KEY=... export WIKI_START_PAGE=docs/getting-started npm run cli -- --output ./docs.pdf # or node src/cli.js --url https://wiki.example.com --start docs --output docs.pdf ``` `WIKI_API_KEY` is read from the environment; `--api-key` remains available for one-off local runs. Use `npm run cli -- --help` for all options. The container includes the CLI too: ```bash podman exec -it wiki-print-wiki-print node src/cli.js --help ``` ## Development and tests ```bash npm install npm test ``` The tests cover path/locale selection, subtree boundaries, link rewriting and sanitization. An API test starts the server against a mock Wiki.js and checks `/api/config`, `/api/pages`, the page sorting and the missing-`WIKI_URL` handling. The smoke test runs a real export and inspects the resulting PDF (text, internal link annotations and external URL annotations). It needs Chromium; `CHROMIUM_PATH` can point to a local installation. To run the full suite in the container: ```bash ./scripts/test.sh ``` This uses the `test` target in the `Containerfile`, which includes dev dependencies. ## Architecture ```text public/ web UI (vanilla HTML/CSS/JS) src/server.js Express server, page list, job API, optional Basic auth src/jobs.js in-memory queue and job status src/exporter.js orchestration: GraphQL → subtree → HTML → PDF src/wiki-client.js Wiki.js GraphQL client src/page-selection.js starting page, locale and subtree resolution src/html-builder.js sanitization, link rewriting, image embedding, print HTML src/pdf-renderer.js Chromium/Playwright print-to-PDF src/print-styles.js print stylesheet podman/ Podman pod manifest scripts/ build/run/test helpers test/ unit tests, mock wiki and PDF smoke test ``` Jobs are stored in memory. Restarting the container removes queued jobs and makes old download links unavailable; finished PDFs live in a temporary directory inside the container and expire after `JOB_TTL_MINUTES`. ## Limitations - Wiki.js 2.x GraphQL API only. - The exporter uses Wiki.js's rendered page HTML. Client-side-only widgets (for example embedded editors or scripts) will not run inside the PDF. Server-rendered diagrams and images work. - Page order is lexicographic by path, with the starting page first. - Very large exports are limited by `MAX_PAGES` and container memory. Chromium needs roughly 200–500 MB for typical documents. - PDF internal links depend on the reader; external URLs are standard PDF URI actions. - The UI itself has no login unless `AUTH_USER`/`AUTH_PASSWORD` is set. Run it only on a trusted network or enable the optional Basic auth.