cv-builder.md (outline-driven Content tab, DnD, rich text, preview page nav, public-CV routing fix + deployment note), cv-theme-engine.md (rich text/ATS/ print), roadmap Phase 4.5 shipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.3 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 (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 variables from these plus the variant's runtime overrides (accent, fonts,
density, page size, photo, icons) and picks one of the layout wrappers. SidebarSections decides which
section keys move to the sidebar for the two-column layouts.
Adding a theme
- Append one
CvTheme { … }toCvThemeCatalog.Themes(Models/CvTheme.cs). Only override the fields that differ from the defaults. - Nothing else. It appears in
GET /api/cv/themes, the Customize tab picker, and renders.
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: the renderer emits
break-inside: avoidon entries,break-after: avoidon section headings, and widow/orphan control, so entries don't split across a page in the Playwright PDF pass.
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.