265 lines
11 KiB
Markdown
265 lines
11 KiB
Markdown
# 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
|
||
`<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-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.
|