Adds LetterHtml.Render, a pure composer mirroring the FE letter canvas'
class vocabulary, behind two ExcludeFromDescription()'d endpoints
(GET /brief/preview, GET /admin/org-template/{subOrgId}/preview).
Auto-resolvable placeholders pull from seed/case data; unresolved
manual ones render as "[NOG IN TE VULLEN: label]". A sent brief
archives its composed HTML (BriefEntity.ArchivedHtml) so a later
org-template republish never changes it. FE gets a hand-written fetch
adapter (text/html, not JSON) and a "Voorbeeld" button that opens the
preview in a new tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.8 KiB
WP-25 — Server-rendered letter preview (HTML; PDF seam deferred)
Status: done Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
Why
"What you compose is what is sent" needs a server-side rendering of the letter — placeholders resolved, org template applied — from the same CSS contract the canvas uses (PRD §2b: one rendering, used twice). The preview is the artifact: at send, the same composition is archived with the brief, making sent letters immutable.
Read first
- PRD Brief v2 §2b, §8;
docs/backlog/WP-24-letter-canvas.md(theletter.csscontract) backend/src/BigRegister.Api/Program.cs— uploadcontentendpoint (binary house pattern:.ExcludeFromDescription()+ hand-written FE fetch)src/app/shared/upload/upload.adapter.ts(hand-written transport precedent)
Decisions (pre-made, don't relitigate)
- HTML, not PDF (user decision at plan review): no Microsoft.Playwright/Chromium
dependency in the POC.
GET /api/v1/brief/previewreturnstext/html— the fully composed, print-ready letter (@pageCSS; browser print-to-PDF is the manual affordance). The endpoint is the seam where a headless-Chromium PDF render slots in later; mark it// ponytail: HTML today, Chromium PDF behind this same route if the POC ever needs real PDF bytes. LetterHtml.Render(brief, orgTemplate)is a pure static composer: mirrors the canvas class vocabulary exactly, inlinespublic/letter.cssfrom disk, inlines the logo bytes as a data-URI. Placeholders: auto-resolvable keys resolve from seed/case data; unresolved manual keys render as[NOG IN TE VULLEN: label](PRD §8) — preview is allowed with errors, only send blocks on them.- Parity is tested, not hoped for: a golden-file test snapshots the composed
HTML; a second test asserts every
letter-prefixed class in the golden HTML exists inletter.css.dotnet testnever launches a browser. - Archive at send:
Sendstores the composed HTML inBriefEntity.ArchivedHtml(SQLite text column) alongside the WP-23 version pin; the preview endpoint serves the archive when status issent, so a republish never changes a sent letter. - Two endpoints, both excluded from OpenAPI (JSON-only generated client stays
clean):
GET /brief/previewandGET /admin/org-template/{subOrgId}/preview(proefbrief: draft template + a fixture brief). FE consumes them via a small hand-written fetch (needs theX-Roleheader) → blob → object URL in a new tab. - Watermark: previews of unsent letters carry a
VOORBEELDwatermark (CSS), the archived/sent rendering never does — the PRD's open question resolved the simple way.
Files
backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs(new)backend/src/BigRegister.Api/Data/BriefStore.cs—ArchivedHtml+ archive at send (+migration)backend/src/BigRegister.Api/Program.cs— 2 preview endpointsbackend/tests/BigRegister.Tests/LetterHtmlTests.cs(new) +LetterHtml.golden.htmlsrc/app/brief/infrastructure/letter-preview.adapter.ts(new, fetch →Result<string, Blob>)src/app/brief/application/brief.store.ts—previewLetter()commandsrc/app/brief/ui/letter-composer/*— "Voorbeeld" button
Steps
LetterHtml.Render+ placeholder resolution + data-URI logo + watermark flag.- Golden-file + class-parity tests.
- Endpoints (serve archive when sent; proefbrief renders the draft template).
- Archive-at-send in
BriefStore.Send(+ migration forArchivedHtml). - FE adapter + store command + button (explicit action — no live re-render; PRD §8).
Acceptance criteria
- Preview opens the composed print-ready letter in a new tab; browser print
shows correct margins via
@page. - Unresolved manual placeholders render
[NOG IN TE VULLEN: …]; preview works despite lint errors (only send blocks). - A sent brief serves its archived HTML unchanged after an org-template republish.
- Golden + parity tests green without any browser installed.
swagger.jsonunchanged by the two endpoints (drift check green).
Verification
cd backend && dotnet test; GREEN one-liner; manual: compose → preview → print
dialog; send → republish template → preview still the archived rendering.
Out of scope
Real PDF bytes / headless Chromium (the deliberate deferral — the endpoint is the seam). Pixel-parity testing (the shared CSS + class-parity test is the fence). Pagination fidelity beyond the browser's own print engine.
Risks
LetterHtml reads public/letter.css from disk — path must resolve for dotnet run,
tests, and docker (bind mount/copy); fail loudly with a clear error if missing.
The golden file will churn whenever the letter structure changes — that is its job;
update it deliberately, never blindly.