Files
2026-11-13 11:50:48 +01:00

265 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.