Files
Resume-Matcher/docs/template-system.md
srbhr 6f7fe141b8 feat: add cover letter and outreach message features to resume management
- Introduced functionality to generate and update cover letters and outreach messages for resumes.
- Enhanced the resume schema to include fields for cover letter and outreach message content.
- Implemented new API endpoints for updating cover letters and outreach messages.
- Added UI components for editing and previewing cover letters and outreach messages in the Resume Builder.
- Updated feature configuration settings to enable or disable cover letter and outreach message generation.
- Enhanced documentation to reflect new features and usage instructions for cover letter and outreach message functionalities.
2025-12-27 02:18:08 +05:30

32 KiB
Raw Blame History

Template System Documentation

This document provides comprehensive guidance for understanding, extending, and creating new resume templates in the Resume Matcher application.


Table of Contents

  1. Current Template Architecture
  2. Template File Structure
  3. CSS Custom Properties System
  4. Creating a New Template
  5. Template Settings Integration
  6. Print & PDF Considerations
  7. Template Examples

1. Current Template Architecture

Available Templates

Template ID Name Layout Status
swiss-single Swiss Single Column Traditional single-column Production
swiss-two-column Swiss Two Column 65%/35% split layout Production

Template Selection Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                         TEMPLATE SELECTION FLOW                             │
└─────────────────────────────────────────────────────────────────────────────┘

User selects template in Builder
         │
         ▼
┌─────────────────┐
│ FormattingCtrls │
│ updates         │
│ templateSettings│
└────────┬────────┘
         │
         ▼
┌─────────────────┐     ┌─────────────────┐
│ ResumeBuilder   │────>│ Resume          │
│ passes settings │     │ component       │
└─────────────────┘     └────────┬────────┘
                                 │
                   ┌─────────────┴─────────────┐
                   │                           │
                   ▼                           ▼
          ┌─────────────────┐        ┌─────────────────┐
          │ swiss-single    │        │ swiss-two-column│
          │ ResumeSingle    │        │ ResumeTwoColumn │
          │ Column.tsx      │        │ .tsx            │
          └─────────────────┘        └─────────────────┘

Component Hierarchy

Resume (components/dashboard/resume-component.tsx)
├── Delegates to template based on settings.template
│
├── ResumeSingleColumn (components/resume/resume-single-column.tsx)
│   ├── PersonalInfoSection
│   ├── ExperienceSection
│   ├── EducationSection
│   ├── ProjectsSection
│   └── SkillsSection
│
└── ResumeTwoColumn (components/resume/resume-two-column.tsx)
    ├── LeftColumn (65%)
    │   ├── PersonalInfoSection
    │   ├── ExperienceSection (sidebar items)
    │   └── ProjectsSection
    │
    └── RightColumn (35%)
        ├── EducationSection
        └── SkillsSection

2. Template File Structure

Directory Layout

apps/frontend/
├── components/
│   ├── dashboard/
│   │   └── resume-component.tsx      # Main router component
│   │
│   └── resume/
│       ├── index.ts                  # Barrel export
│       ├── resume-single-column.tsx  # Swiss single column
│       ├── resume-two-column.tsx     # Swiss two column
│       └── sections/                 # Shared section components
│           ├── personal-info.tsx
│           ├── experience.tsx
│           ├── education.tsx
│           ├── projects.tsx
│           └── skills.tsx
│
├── lib/
│   ├── types/
│   │   └── template-settings.ts      # Settings types & defaults
│   │
│   └── constants/
│       └── page-dimensions.ts        # A4/Letter dimensions
│
└── app/
    └── (default)/
        └── css/
            └── globals.css           # CSS custom properties

Key Files

Resume Router (components/dashboard/resume-component.tsx)

// Lines 1-50 - Main resume component that routes to templates
interface ResumeProps {
  data: ResumeData;
  settings?: TemplateSettings;
  className?: string;
}

export function Resume({ data, settings, className }: ResumeProps) {
  const templateSettings = settings ?? DEFAULT_TEMPLATE_SETTINGS;

  // Apply CSS custom properties
  const style = getTemplateStyles(templateSettings);

  switch (templateSettings.template) {
    case "swiss-single":
      return <ResumeSingleColumn data={data} style={style} className={className} />;
    case "swiss-two-column":
      return <ResumeTwoColumn data={data} style={style} className={className} />;
    default:
      return <ResumeSingleColumn data={data} style={style} className={className} />;
  }
}

Template Settings Types (lib/types/template-settings.ts)

// Lines 1-80 - Complete type definitions

export type TemplateType = "swiss-single" | "swiss-two-column";
export type PageSize = "A4" | "LETTER";

export interface TemplateSettings {
  template: TemplateType;
  pageSize: PageSize;
  margins: {
    top: number;    // mm
    bottom: number;
    left: number;
    right: number;
  };
  sectionSpacing: number;  // 1-5 scale
  itemSpacing: number;     // 1-5 scale
  lineHeight: number;      // 1-5 scale
  fontSize: number;        // 1-5 scale
  headerScale: number;     // 1-5 scale
}

export const DEFAULT_TEMPLATE_SETTINGS: TemplateSettings = {
  template: "swiss-single",
  pageSize: "A4",
  margins: { top: 15, bottom: 15, left: 15, right: 15 },
  sectionSpacing: 3,
  itemSpacing: 3,
  lineHeight: 3,
  fontSize: 3,
  headerScale: 3,
};

// CSS custom property mapping
export const SPACING_VALUES = {
  sectionSpacing: ["0.75rem", "1rem", "1.25rem", "1.5rem", "2rem"],
  itemSpacing: ["0.25rem", "0.5rem", "0.75rem", "1rem", "1.25rem"],
  lineHeight: ["1.2", "1.3", "1.4", "1.5", "1.6"],
  fontSize: ["0.8rem", "0.875rem", "1rem", "1.125rem", "1.25rem"],
  headerScale: ["1.5", "1.75", "2", "2.25", "2.5"],
};

export function getTemplateStyles(settings: TemplateSettings): CSSProperties {
  return {
    "--section-spacing": SPACING_VALUES.sectionSpacing[settings.sectionSpacing - 1],
    "--item-spacing": SPACING_VALUES.itemSpacing[settings.itemSpacing - 1],
    "--line-height": SPACING_VALUES.lineHeight[settings.lineHeight - 1],
    "--font-size": SPACING_VALUES.fontSize[settings.fontSize - 1],
    "--header-scale": SPACING_VALUES.headerScale[settings.headerScale - 1],
  } as CSSProperties;
}

3. CSS Custom Properties System

Global CSS Variables (globals.css)

/* Resume Template Variables */
:root {
  /* Spacing */
  --section-spacing: 1.25rem;
  --item-spacing: 0.75rem;

  /* Typography */
  --line-height: 1.4;
  --font-size: 1rem;
  --header-scale: 2;

  /* Colors (Swiss Design) */
  --resume-bg: #FFFFFF;
  --resume-text: #1a1a1a;
  --resume-accent: #000000;
  --resume-muted: #666666;
  --resume-border: #E5E5E0;
}

/* Resume Base Styles */
.resume-container {
  font-size: var(--font-size);
  line-height: var(--line-height);
  color: var(--resume-text);
  background: var(--resume-bg);
}

.resume-section {
  margin-bottom: var(--section-spacing);
}

.resume-item {
  margin-bottom: var(--item-spacing);
}

.resume-header {
  font-size: calc(var(--font-size) * var(--header-scale));
}

/* Page Break Control */
.resume-section {
  break-inside: avoid;
}

.resume-item {
  break-inside: avoid;
}

.resume-section-title {
  break-after: avoid;
}

How Settings Map to CSS

┌─────────────────────────────────────────────────────────────────────────────┐
│                    SETTINGS → CSS MAPPING                                   │
└─────────────────────────────────────────────────────────────────────────────┘

TemplateSettings                         CSS Custom Properties
─────────────────────────────────────────────────────────────────────────────

sectionSpacing: 3  ──────────────────>  --section-spacing: 1.25rem
                                        (index 2 of SPACING_VALUES array)

itemSpacing: 3     ──────────────────>  --item-spacing: 0.75rem

lineHeight: 3      ──────────────────>  --line-height: 1.4

fontSize: 3        ──────────────────>  --font-size: 1rem

headerScale: 3     ──────────────────>  --header-scale: 2
                                        (h1 becomes 2rem with --font-size: 1rem)

Value Scales

Setting Level 1 Level 2 Level 3 Level 4 Level 5
Section Spacing 0.75rem 1rem 1.25rem 1.5rem 2rem
Item Spacing 0.25rem 0.5rem 0.75rem 1rem 1.25rem
Line Height 1.2 1.3 1.4 1.5 1.6
Font Size 0.8rem 0.875rem 1rem 1.125rem 1.25rem
Header Scale 1.5x 1.75x 2x 2.25x 2.5x

Bold = Default (Level 3)


4. Creating a New Template

Step 1: Define Template ID

Add the new template type to lib/types/template-settings.ts:

// Before
export type TemplateType = "swiss-single" | "swiss-two-column";

// After
export type TemplateType = "swiss-single" | "swiss-two-column" | "modern-minimal";

Step 2: Create Template Component

Create a new file components/resume/resume-modern-minimal.tsx:

import { CSSProperties } from "react";
import { ResumeData } from "@/lib/types/resume";

interface Props {
  data: ResumeData;
  style?: CSSProperties;
  className?: string;
}

export function ResumeModernMinimal({ data, style, className }: Props) {
  return (
    <div
      className={`resume-container ${className ?? ""}`}
      style={style}
    >
      {/* Header Section */}
      <header className="resume-section border-b-2 border-black pb-4 mb-6">
        <h1 className="resume-header font-bold tracking-tight">
          {data.personal_info.name}
        </h1>
        <div className="flex gap-4 text-sm mt-2">
          {data.personal_info.email && (
            <span>{data.personal_info.email}</span>
          )}
          {data.personal_info.phone && (
            <span>{data.personal_info.phone}</span>
          )}
          {data.personal_info.location && (
            <span>{data.personal_info.location}</span>
          )}
        </div>
      </header>

      {/* Summary */}
      {data.personal_info.summary && (
        <section className="resume-section">
          <p className="text-gray-700 leading-relaxed">
            {data.personal_info.summary}
          </p>
        </section>
      )}

      {/* Experience */}
      {data.experience && data.experience.length > 0 && (
        <section className="resume-section">
          <h2 className="text-lg font-bold uppercase tracking-widest mb-3">
            Experience
          </h2>
          {data.experience.map((exp, i) => (
            <article key={i} className="resume-item">
              <div className="flex justify-between items-baseline">
                <h3 className="font-semibold">{exp.title}</h3>
                <span className="text-sm text-gray-500">
                  {exp.start_date} - {exp.end_date || "Present"}
                </span>
              </div>
              <p className="text-gray-600">{exp.company}</p>
              {exp.bullets && (
                <ul className="mt-2 space-y-1">
                  {exp.bullets.map((bullet, j) => (
                    <li key={j} className="text-sm pl-4 relative before:content-[''] before:absolute before:left-0">
                      {bullet}
                    </li>
                  ))}
                </ul>
              )}
            </article>
          ))}
        </section>
      )}

      {/* Education */}
      {data.education && data.education.length > 0 && (
        <section className="resume-section">
          <h2 className="text-lg font-bold uppercase tracking-widest mb-3">
            Education
          </h2>
          {data.education.map((edu, i) => (
            <article key={i} className="resume-item">
              <div className="flex justify-between items-baseline">
                <h3 className="font-semibold">{edu.degree}</h3>
                <span className="text-sm text-gray-500">
                  {edu.graduation_date}
                </span>
              </div>
              <p className="text-gray-600">{edu.institution}</p>
            </article>
          ))}
        </section>
      )}

      {/* Skills */}
      {data.skills && (
        <section className="resume-section">
          <h2 className="text-lg font-bold uppercase tracking-widest mb-3">
            Skills
          </h2>
          <p className="text-sm">
            {Array.isArray(data.skills)
              ? data.skills.join(" • ")
              : data.skills}
          </p>
        </section>
      )}
    </div>
  );
}

Step 3: Register in Router

Update components/dashboard/resume-component.tsx:

import { ResumeModernMinimal } from "@/components/resume/resume-modern-minimal";

// In the switch statement:
switch (templateSettings.template) {
  case "swiss-single":
    return <ResumeSingleColumn data={data} style={style} className={className} />;
  case "swiss-two-column":
    return <ResumeTwoColumn data={data} style={style} className={className} />;
  case "modern-minimal":
    return <ResumeModernMinimal data={data} style={style} className={className} />;
  default:
    return <ResumeSingleColumn data={data} style={style} className={className} />;
}

Step 4: Add to Template Selector UI

Update components/builder/formatting-controls.tsx:

const TEMPLATES = [
  {
    id: "swiss-single",
    name: "Single Column",
    preview: "/templates/swiss-single.png",
  },
  {
    id: "swiss-two-column",
    name: "Two Column",
    preview: "/templates/swiss-two-column.png",
  },
  {
    id: "modern-minimal",
    name: "Modern Minimal",
    preview: "/templates/modern-minimal.png",
  },
];

Step 5: Export from Index

Update components/resume/index.ts:

export { ResumeSingleColumn } from "./resume-single-column";
export { ResumeTwoColumn } from "./resume-two-column";
export { ResumeModernMinimal } from "./resume-modern-minimal";

5. Template Settings Integration

Builder Integration

The ResumeBuilder component manages template settings through state:

// components/builder/resume-builder.tsx

const [templateSettings, setTemplateSettings] = useState<TemplateSettings>(
  DEFAULT_TEMPLATE_SETTINGS
);

// Load from localStorage on mount
useEffect(() => {
  const saved = localStorage.getItem("resume_builder_settings");
  if (saved) {
    setTemplateSettings(JSON.parse(saved));
  }
}, []);

// Save to localStorage on change
useEffect(() => {
  localStorage.setItem(
    "resume_builder_settings",
    JSON.stringify(templateSettings)
  );
}, [templateSettings]);

FormattingControls Component

// components/builder/formatting-controls.tsx

interface FormattingControlsProps {
  settings: TemplateSettings;
  onChange: (settings: TemplateSettings) => void;
}

export function FormattingControls({ settings, onChange }: FormattingControlsProps) {
  return (
    <div className="space-y-4 p-4 border rounded-lg">
      {/* Template Selection */}
      <div>
        <label className="block text-sm font-medium mb-2">Template</label>
        <div className="grid grid-cols-2 gap-2">
          {TEMPLATES.map((template) => (
            <button
              key={template.id}
              onClick={() => onChange({ ...settings, template: template.id })}
              className={`p-2 border rounded ${
                settings.template === template.id
                  ? "border-blue-500 bg-blue-50"
                  : "border-gray-200"
              }`}
            >
              <img src={template.preview} alt={template.name} />
              <span className="text-xs">{template.name}</span>
            </button>
          ))}
        </div>
      </div>

      {/* Page Size */}
      <div>
        <label className="block text-sm font-medium mb-2">Page Size</label>
        <div className="flex gap-2">
          <button
            onClick={() => onChange({ ...settings, pageSize: "A4" })}
            className={`px-3 py-1 rounded ${
              settings.pageSize === "A4" ? "bg-black text-white" : "bg-gray-100"
            }`}
          >
            A4
          </button>
          <button
            onClick={() => onChange({ ...settings, pageSize: "LETTER" })}
            className={`px-3 py-1 rounded ${
              settings.pageSize === "LETTER" ? "bg-black text-white" : "bg-gray-100"
            }`}
          >
            US Letter
          </button>
        </div>
      </div>

      {/* Spacing Sliders */}
      <div>
        <label className="block text-sm font-medium mb-2">
          Section Spacing: {settings.sectionSpacing}
        </label>
        <input
          type="range"
          min="1"
          max="5"
          value={settings.sectionSpacing}
          onChange={(e) =>
            onChange({ ...settings, sectionSpacing: parseInt(e.target.value) })
          }
          className="w-full"
        />
      </div>

      {/* ... More controls */}
    </div>
  );
}

PDF Download with Settings

Settings are passed to the PDF endpoint:

// lib/api/resume.ts

export async function downloadResumePdf(
  resumeId: string,
  settings: TemplateSettings
): Promise<Blob> {
  const params = new URLSearchParams({
    template: settings.template,
    pageSize: settings.pageSize,
    marginTop: settings.margins.top.toString(),
    marginBottom: settings.margins.bottom.toString(),
    marginLeft: settings.margins.left.toString(),
    marginRight: settings.margins.right.toString(),
    sectionSpacing: settings.sectionSpacing.toString(),
    itemSpacing: settings.itemSpacing.toString(),
    lineHeight: settings.lineHeight.toString(),
    fontSize: settings.fontSize.toString(),
    headerScale: settings.headerScale.toString(),
  });

  const response = await fetch(
    `${API_BASE}/resumes/${resumeId}/pdf?${params}`
  );

  if (!response.ok) {
    throw new Error("Failed to download PDF");
  }

  return response.blob();
}

6. Print & PDF Considerations

Print Route

The frontend has a dedicated print route for PDF rendering:

apps/frontend/app/print/resumes/[id]/page.tsx

This page:

  1. Receives resume ID and settings via URL params
  2. Fetches resume data from backend
  3. Renders the Resume component with settings
  4. Playwright visits this page and generates PDF

Print Styles

/* globals.css - Print-specific styles */

@media print {
  .resume-container {
    width: 100%;
    max-width: none;
    padding: 0;
    margin: 0;
  }

  .resume-section {
    break-inside: avoid;
  }

  .resume-item {
    break-inside: avoid;
  }

  /* Hide non-print elements */
  .no-print {
    display: none !important;
  }
}

Playwright PDF Configuration

# apps/backend/app/pdf.py

async def render_resume_pdf(
    resume_id: str,
    template: str,
    page_size: str,
    margins: dict,
    **kwargs
) -> bytes:
    # Build URL with all settings
    params = urlencode({
        "template": template,
        "pageSize": page_size,
        **{f"margin{k.title()}": v for k, v in margins.items()},
        **kwargs,
    })

    url = f"{FRONTEND_URL}/print/resumes/{resume_id}?{params}"

    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        await page.goto(url, wait_until="networkidle")

        # Wait for fonts to load
        await page.wait_for_function("document.fonts.ready")

        pdf_bytes = await page.pdf(
            format=page_size,
            margin={"top": "0mm", "right": "0mm", "bottom": "0mm", "left": "0mm"},
            print_background=True,
            prefer_css_page_size=False,
        )

        await browser.close()

    return pdf_bytes

Margin Application

Margins are applied in the HTML, not Playwright:

// app/print/resumes/[id]/page.tsx

export default function PrintResumePage({ searchParams }) {
  const { marginTop = 15, marginBottom = 15, marginLeft = 15, marginRight = 15 } = searchParams;

  return (
    <div
      style={{
        padding: `${marginTop}mm ${marginRight}mm ${marginBottom}mm ${marginLeft}mm`,
        width: searchParams.pageSize === "A4" ? "210mm" : "215.9mm",
        minHeight: searchParams.pageSize === "A4" ? "297mm" : "279.4mm",
      }}
    >
      <Resume data={resumeData} settings={settings} />
    </div>
  );
}

7. Template Examples

Swiss Single Column

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  JOHN DOE                                                       │
│  john@email.com | (555) 123-4567 | New York, NY                │
│  linkedin.com/in/johndoe | github.com/johndoe                  │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  PROFESSIONAL SUMMARY                                           │
│  Experienced software engineer with 8+ years building          │
│  scalable web applications...                                   │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  EXPERIENCE                                                     │
│                                                                 │
│  Senior Software Engineer                                       │
│  Tech Company Inc. | Jan 2020 - Present                        │
│  • Led team of 5 engineers...                                  │
│  • Improved performance by 40%...                              │
│                                                                 │
│  Software Engineer                                              │
│  Startup Co. | Jun 2016 - Dec 2019                             │
│  • Built microservices architecture...                         │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  EDUCATION                                                      │
│                                                                 │
│  B.S. Computer Science                                          │
│  University of Technology | 2016                                │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  SKILLS                                                         │
│  Python, JavaScript, React, Node.js, PostgreSQL, AWS           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Swiss Two Column

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  JOHN DOE                                                       │
│  john@email.com | (555) 123-4567 | New York, NY                │
│                                                                 │
├─────────────────────────────────────────┬───────────────────────┤
│                                         │                       │
│  EXPERIENCE                             │  EDUCATION            │
│                                         │                       │
│  Senior Software Engineer               │  B.S. Computer Sci.   │
│  Tech Company Inc.                      │  Univ. of Technology  │
│  Jan 2020 - Present                     │  2016                 │
│  • Led team of 5 engineers...           │                       │
│  • Improved performance by 40%...       │  ─────────────────────│
│                                         │                       │
│  Software Engineer                      │  SKILLS               │
│  Startup Co.                            │                       │
│  Jun 2016 - Dec 2019                    │  Languages:           │
│  • Built microservices...               │  Python, JavaScript   │
│                                         │                       │
│  ─────────────────────────────────────  │  Frameworks:          │
│                                         │  React, Node.js       │
│  PROJECTS                               │                       │
│                                         │  Databases:           │
│  Open Source CLI Tool                   │  PostgreSQL, MongoDB  │
│  github.com/johndoe/cli-tool            │                       │
│  • Built popular CLI with 1k+ stars     │  Cloud:               │
│                                         │  AWS, GCP             │
│                                         │                       │
└─────────────────────────────────────────┴───────────────────────┘

Modern Minimal (Example New Template)

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  john doe                                                       │
│  ═══════════════════════════════════════════════════════════   │
│  john@email.com  •  (555) 123-4567  •  New York, NY            │
│                                                                 │
│                                                                 │
│  Experienced software engineer with 8+ years building          │
│  scalable web applications using modern technologies.          │
│                                                                 │
│                                                                 │
│  E X P E R I E N C E                                           │
│                                                                 │
│  Senior Software Engineer                       2020  Present  │
│  Tech Company Inc.                                              │
│   Led team of 5 engineers on customer platform                │
│   Improved API response time by 40%                           │
│                                                                 │
│  Software Engineer                              2016  2019     │
│  Startup Co.                                                    │
│   Built microservices architecture from scratch               │
│                                                                 │
│                                                                 │
│  E D U C A T I O N                                             │
│                                                                 │
│  B.S. Computer Science                                    2016  │
│  University of Technology                                       │
│                                                                 │
│                                                                 │
│  S K I L L S                                                   │
│                                                                 │
│  Python • JavaScript • React • Node.js • PostgreSQL • AWS      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Template Checklist

When creating a new template, ensure:

  • Template ID added to TemplateType union
  • Component file created in components/resume/
  • Component exported from components/resume/index.ts
  • Switch case added in resume-component.tsx
  • Preview thumbnail created (300x400px recommended)
  • Template added to TEMPLATES array in formatting controls
  • CSS custom properties used for spacing/typography
  • .resume-section and .resume-item classes applied
  • Page break rules respected
  • Print styles tested
  • PDF generation tested with all page sizes
  • All ResumeData fields handled (including optional ones)

Files Changed Summary

For Adding a New Template

File Change
lib/types/template-settings.ts Add to TemplateType union
components/resume/[new-template].tsx Create new component
components/resume/index.ts Export new component
components/dashboard/resume-component.tsx Add switch case
components/builder/formatting-controls.tsx Add to TEMPLATES array
public/templates/[new-template].png Add preview thumbnail

For Modifying Template System

File Purpose
lib/types/template-settings.ts Settings types, defaults, CSS mapping
app/(default)/css/globals.css CSS custom properties, print styles
components/builder/resume-builder.tsx Settings state management
components/builder/formatting-controls.tsx Settings UI
app/print/resumes/[id]/page.tsx Print route with margins
apps/backend/app/pdf.py Playwright PDF generation