11 KiB
wiki-print
Export a Wiki.js 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
#anchorlinks 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.
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.
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:
./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:
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
<start-path>/ 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-anchorin 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:
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-Idresponse 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
debuglevel. - Image embedding counts at
debuglevel. - 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:
{"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:
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:
podman exec -it wiki-print-wiki-print node src/cli.js --help
Development and tests
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:
./scripts/test.sh
This uses the test target in the Containerfile, which includes dev dependencies.
Architecture
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_PAGESand 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_PASSWORDis set. Run it only on a trusted network or enable the optional Basic auth.