3.9 KiB
CV Theme Engine
Phase 4 (2026-07-18). How CV themes work and how to add one. Companion to
docs/architecture/cv-builder.md. Verified against code.
The rule
A theme is data, not code. There is exactly one renderer — ThemedCvRenderer
(JobTrackerApi/Services/ThemedCvRenderer.cs) — with a single render path. Every visual difference
between themes is expressed by the fields of a CvTheme record (JobTrackerApi/Models/CvTheme.cs). Adding a theme
never touches the renderer.
This replaces the previous approach (CvTemplateRenderer, one hand-written HTML method per template),
which is retained only for the legacy tailored-draft flow and is not used by the builder.
CvTheme fields (the knobs)
| Group | Fields |
|---|---|
| Identity | Id, Name, Category, Description |
| Layout | Layout (single | sidebar-left | sidebar-right | header-band), SidebarWidthMm, SidebarSections |
| Palette | Accent, Ink, Muted, Line, Paper, SidebarBg, SidebarInk, HeadingColor |
| Typography | HeadingFont, BodyFont, NameSizePt, HeadingSizePt, BodySizePt, LineHeight |
| Spacing | PageMarginMm, SectionGapMm, EntryGapMm |
| Styling | HeaderStyle (plain|band|centered|kicker), HeadingStyle (caps-rule|underline|plain|bar), PhotoShape (none|square|rounded|circle), DefaultIcons |
The renderer computes CSS from these plus normalized variant overrides: printable palette, curated
fonts, body/heading size, line height, spacing, page size, layout, sidebar width/content, header and
heading treatments, skills presentation, photo and icons. SidebarSections decides which section
keys move to the sidebar for the two-column layouts. Changing a template or override never transforms
the structured career content.
Adding a theme
- Append one
CvTheme { … }toCvThemeCatalog.Themes(JobTrackerApi/Models/CvTheme.cs). Only override the fields that differ from the defaults. - Nothing else. It appears in
GET /api/cv/themes, the Template tab and dashboard create flow.GET /api/cv/themes/{id}/previewrenders isolated in-memory sample data for a real visual thumbnail; preview data is never persisted.
Add a CvBuilderTests.Every_catalog_theme_renders_valid_html already loops the whole catalog, so a new
theme is smoke-tested automatically.
Shipped themes (8)
modern (header-band), minimal (single), executive (single, serif), technical (sidebar-left,
dense), ats-classic (single, no graphics), nordic (sidebar-right), elegant (single, editorial),
creative (sidebar-left, bold).
Rich text, ATS, and print (2026-07-18)
- Rich-text bullets are shipped: bullets/summary support
**bold**,*italic*,__underline__,[text](url)viaThemedCvRenderer.Inline— everything is HTML-escaped first, then a whitelist is re-applied, so no user tag survives. Storage stays plain text (no HTML, no sanitiser dependency). - ATS-friendliness is a data flag:
CvTheme.AtsFriendly(set on the single-column themes), surfaced inGET /api/cv/themesand badged in the Customize tab. Two-column (sidebar) themes are not flagged, as sidebar layouts can trip naive resume parsers. - Print quality: ordinary entries use
break-inside: avoid-page; content classified as too tall for a page is allowed to flow between bullets/paragraphs so Chromium cannot clip an unsplittable block. Section headings avoid a following break, list items carry widow/orphan rules, and long names/titles/contact values/URLs/tags wrap withinminmax(0, …)columns. Typography is not scaled down to mask overflow.
Deliberately not here (yet)
- PDF page numbers.
ShowPageNumbersis carried through settings but the actual footer is a PlaywrightfooterTemplateconcern (the PDF exporter), not CSS paged-media (Chromium's margin boxes are unreliable). Wire it inPlaywrightCvPdfExporterwhen needed.