When I started building a resume builder, I did what most developers do: I created one React component per template. TemplateModern.tsx, TemplateClassic.tsx, TemplateMinimal.tsx. Each one was a slightly tweaked copy of the last.
By template five, the pattern was already painful. A bug fix in the header meant five identical changes. A new section type meant touching every component. Worse, adding a new template required duplicating hundreds of lines of JSX just to change some colors and a font.
There had to be a better way.
The Core Insight: Templates Are Data, Not Code
Most resume templates are not structurally different. They share the same sections — header, experience, education, skills. What varies is how those sections are rendered: column layout, font choices, color palette, the way section headers look, how skills are displayed.
That variation is configuration, not logic. So I stopped writing components and started writing config objects.
The TemplateConfig Interface
Every template in the system is defined by a single TypeScript object:
interface TemplateConfig {
id: string;
name: string;
layout: 'single-column' | 'two-column-left-sidebar' | 'two-column-right-sidebar';
headerStyle: 'centered-text' | 'sidebar-header' | 'full-bleed-color';
sectionHeaderStyle: 'border-bottom' | 'left-border' | 'pill-header';
skillsStyle: 'pills' | 'progress-bar' | 'tags-code' | 'comma-list';
colors: ColorPalette;
typography: TypographyConfig;
spacing: SpacingConfig;
}
No JSX. No component. Just a plain object describing how the resume should look.
From 10 Templates to 160+
With config-driven templates, 160 templates is 160 plain objects — roughly 6,000 lines of clean data. Adding a new template takes about two minutes: pick a layout variant, choose fonts, set a color palette, done. The rendering logic is tested once; the configs just exercise it.
The enum-style union types on layout, headerStyle, and skillsStyle are intentional constraints. Each value maps to exactly one tested rendering path. If a designer asks for a new variant, you add it to the union and implement it once — every template can use it immediately.
Google Fonts: Dynamic Loading
With 160+ templates using different font combinations, bundling all fonts statically is not an option. Fonts are loaded dynamically when a template is selected — keeping the initial bundle lean.
Print-Ready A4 Output
The renderer wraps everything in a fixed A4 container (210mm x 297mm) and uses @media print rules for pixel-perfect PDF exports via window.print() — no headless browser required.
Lessons Learned
Start with the config interface, not the components. Defining your variation axes upfront forces you to think about what actually differs between templates.
Keep enum variants small and orthogonal. Every combination of layout + headerStyle + sectionHeaderStyle needs to look good together.
CSS custom properties are your best friend. Set them as CSS variables at the root and let CSS do the cascading.
I built this for CVBooster, a free resume builder with 160+ templates. You can see the config-driven rendering in action — no signup required. Check out the live result at cvbooster.ai.
Top comments (0)