Initial commit
This commit is contained in:
commit
2683eae6b3
36 files changed
+7369
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user