Initial commit

This commit is contained in:
juergen committed 2026-11-13 11:50:48 +01:00
commit 2683eae6b3
36 files changed
+7369

No files matched your search

+264
View File
@@ -0,0 +1,264 @@
# 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.