PDF export via Gotenberg #67

Closed
opened 2026-07-04 14:52:35 +02:00 by fable-5 · 1 comment
Collaborator

Context

PDF export for reading/sharing, rendered server-side for consistent results (ADR 0009).

Scope

Add Gotenberg to the Compose stack (internal, pinned, healthcheck); PDF export job: render page via the public-HTML renderer (#56) in an export variant (no chrome, pond fonts inlined as base64 @font-face, print CSS: page margins, page numbers, title header), send to Gotenberg's Chromium route, store result for download; wire the page export menu's PDF entry with job progress.

Acceptance criteria

  • exported PDF of the fixture page shows correct structure, fonts, and images
  • plugin blocks render fallbacks once M7 lands (placeholder acceptable now — leave a marked TODO referencing #79)
  • export of a permission-restricted page requires read permission (job ownership test)
  • Gotenberg down → job fails gracefully; readyz warning

Technical notes

  • ADR 0009, ADR 0016 (fonts in PDF).

Dependencies

Depends on #56, #62, #66.

Size: ~1.5 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context PDF export for reading/sharing, rendered server-side for consistent results (ADR 0009). ## Scope Add Gotenberg to the Compose stack (internal, pinned, healthcheck); PDF export job: render page via the public-HTML renderer (#56) in an export variant (no chrome, pond fonts inlined as base64 @font-face, print CSS: page margins, page numbers, title header), send to Gotenberg's Chromium route, store result for download; wire the page export menu's PDF entry with job progress. ## Acceptance criteria - [ ] exported PDF of the fixture page shows correct structure, fonts, and images - [ ] plugin blocks render fallbacks once M7 lands (placeholder acceptable now — leave a marked TODO referencing #79) - [ ] export of a permission-restricted page requires read permission (job ownership test) - [ ] Gotenberg down → job fails gracefully; readyz warning ## Technical notes - ADR 0009, ADR 0016 (fonts in PDF). ## Dependencies Depends on #56, #62, #66. **Size**: ~1.5 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M6 — Import, export & attachments milestone 2026-07-04 14:52:35 +02:00
fable-5 added the
backend
deployment
labels 2026-07-04 14:52:35 +02:00
Collaborator

Done in 8a68ef6. Pipeline fully green (all 7 contexts); the new Gotenberg sidecar is provisioned on both stages and PDF export is verified live.

What shipped (ADR 0009/0016)

  • Sidecar: gotenberg/gotenberg:8 in the Compose stack (internal, pinned, healthcheck); api GOTENBERG_URL; a renderer readyz check at warning-level (mirrors the converter), so PDF export degrades gracefully when Gotenberg is down without failing readyz.
  • Export HTML (buildPdfHtml): a self-contained document (no app chrome) — the page content with images inlined as data: URIs, the pond's fonts inlined as base64 @font-face (read from the catalog now baked into the api image) + applied via CSS variables, print CSS (A4, page-break rules, a title header), and page numbers from Gotenberg's footer.
  • Job flow: POST /pages/:id/export {format: pdf} (read-permission gated) builds the HTML and enqueues an export_pdf job on the #62 queue with the HTML as input; the worker branches to === 'pdf' to the GotenbergRenderer (html → pdf) instead of pandoc, retrying an unreachable sidecar and failing a refused render (renderer_unavailable/render_failed, de+en). The client polls and downloads GET /jobs/:id/result.
  • Frontend: the page-menu PDF button is now a real export (the disabled placeholder is gone).

Acceptance criteria

  • exported PDF shows correct structure, fonts, and images — verified live on Int: a page's PDF is a valid application/pdf (53 KB) with the pond font embedded (FontFile) and its image rendered.
  • plugin blocks render fallbacks once M7 lands — marked TODO(#79) in pdf-html.ts (placeholder acceptable now, per the AC).
  • export of a permission-restricted page requires read permission — the endpoint is @RequiresPagePermission('read'); the resulting job is owner-scoped (a foreign/unknown id is 404, #62).
  • Gotenberg down → job fails gracefully; readyz warning — render_failed job failure (DB test with an injected failing renderer) + the warning-level renderer readyz check.

Fonts in the api image: the api Dockerfile now bakes the font catalog in (build-fonts.mjs with FONTS_OUT) so the exporter can inline a pond's WOFF2; a missing file falls back to the system stack.

Tests: export.service.db PDF cases (HTML carries the title, the font CSS variable, and the inlined image; renderer-down → render_failed) + e2e PDF export (self-skips without a Gotenberg sidecar, like the .docx case). Verified locally against real Gotenberg (valid PDF, font embedded) and live on Int.

Stage provisioning (new sidecar, pattern from #62/#63): the repo Compose gained the gotenberg service; the byte-identical stage Composes on the VPS were replaced (backups *.bak-pre67) and docker compose pull gotenberg && up -d ran on both — Test + Int now report renderer: ok. The api degrades to warn without it, so the CD deploy was never blocked.

Next: #68 (GDPR data export), #69 (QA fidelity gate).

Done in `8a68ef6`. Pipeline fully green (all 7 contexts); the **new Gotenberg sidecar is provisioned on both stages** and PDF export is verified live. **What shipped (ADR 0009/0016)** - **Sidecar**: `gotenberg/gotenberg:8` in the Compose stack (internal, pinned, healthcheck); api `GOTENBERG_URL`; a `renderer` readyz check at **warning-level** (mirrors the converter), so PDF export degrades gracefully when Gotenberg is down without failing readyz. - **Export HTML** (`buildPdfHtml`): a self-contained document (no app chrome) — the page content with images inlined as `data:` URIs, the **pond's fonts inlined as base64 `@font-face`** (read from the catalog now baked into the api image) + applied via CSS variables, print CSS (A4, page-break rules, a title header), and page numbers from Gotenberg's footer. - **Job flow**: `POST /pages/:id/export {format: pdf}` (read-permission gated) builds the HTML and enqueues an `export_pdf` job on the #62 queue with the HTML as input; the worker branches `to === 'pdf'` to the `GotenbergRenderer` (html → pdf) instead of pandoc, retrying an unreachable sidecar and failing a refused render (`renderer_unavailable`/`render_failed`, de+en). The client polls and downloads `GET /jobs/:id/result`. - **Frontend**: the page-menu PDF button is now a real export (the disabled placeholder is gone). **Acceptance criteria** - [x] exported PDF shows correct structure, fonts, and images — verified live on Int: a page's PDF is a valid `application/pdf` (53 KB) with the **pond font embedded** (`FontFile`) and its image rendered. - [x] plugin blocks render fallbacks once M7 lands — marked `TODO(#79)` in `pdf-html.ts` (placeholder acceptable now, per the AC). - [x] export of a permission-restricted page requires read permission — the endpoint is `@RequiresPagePermission('read')`; the resulting job is owner-scoped (a foreign/unknown id is 404, #62). - [x] Gotenberg down → job fails gracefully; readyz warning — `render_failed` job failure (DB test with an injected failing renderer) + the warning-level `renderer` readyz check. **Fonts in the api image**: the api Dockerfile now bakes the font catalog in (`build-fonts.mjs` with `FONTS_OUT`) so the exporter can inline a pond's WOFF2; a missing file falls back to the system stack. **Tests**: `export.service.db` PDF cases (HTML carries the title, the font CSS variable, and the inlined image; renderer-down → `render_failed`) + e2e PDF export (self-skips without a Gotenberg sidecar, like the `.docx` case). Verified locally against real Gotenberg (valid PDF, font embedded) and live on Int. **Stage provisioning** (new sidecar, pattern from #62/#63): the repo Compose gained the `gotenberg` service; the byte-identical stage Composes on the VPS were replaced (backups `*.bak-pre67`) and `docker compose pull gotenberg && up -d` ran on both — Test + Int now report `renderer: ok`. The api degrades to `warn` without it, so the CD deploy was never blocked. Next: #68 (GDPR data export), #69 (QA fidelity gate).
Sign in to join this conversation.
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#67
No description provided.