Files
cv-site/doc/11-PDF-EXPORT.md
T
juanatsap 16194328b6 docs+tests: Document and test references section shortcut URLs
## Documentation Updates (doc/11-PDF-EXPORT.md)
Added "References Section Integration" section with:
- Template code examples for shortcut URL usage
- Data configuration structure
- Key features and use cases
- Security attributes documentation
- Test command reference

## Test Updates (tests/mjs/28-references-pdf-download.test.mjs)
Completely rewrote test to match new direct link behavior:
-  OLD: Tested modal opening, onclick handlers
-  NEW: Tests direct shortcut URLs

New test coverage (5 tests):
1. English direct shortcut URL validation
2. Spanish direct shortcut URL validation
3. HTTP 301 redirect verification
4. Year validation (404 for invalid years)
5. Works without JavaScript (PDF export context)

Verifies:
- Correct href: /cv-jamr-{year}-{lang}.pdf
- No onclick handlers (pure HTML link)
- Security attributes (target="_blank", rel="noopener noreferrer")
- Year-aware dynamic URLs
- Language-aware (es/en)
- Backend 301 redirects working
- 404 for past/future years

## Benefits
- Comprehensive test coverage for new behavior
- Complete documentation with examples
- Easy to maintain and understand
- Validates entire shortcut URL feature end-to-end
2025-11-20 12:43:29 +00:00

34 KiB

PDF Export Feature Documentation

Overview

The CV application provides a comprehensive PDF export system with three predefined options and dynamic filename generation. Users can download their CV in different formats through an interactive modal interface with an intuitive, clear naming convention.

🎯 Architecture: Dual Rendering Modes

Critical Design Decision: The PDF generator uses different media emulation depending on the version:

Clean Version (Short CV) - Print Mode

  • Uses: @media print CSS rules (default browser print behavior)
  • Purpose: Print-friendly, minimal layout
  • Characteristics:
    • No skills sidebars (hidden by print.css)
    • No UI elements (navigation, buttons, footer hidden by print.css)
    • Print-optimized typography, margins, and page breaks
    • ~4 pages, ~2.2 MB
    • Clean, professional print appearance

Extended Version (Long CV) - Screen Mode + UI Hiding

  • Uses: @media screen CSS (emulated via CDP) + injected CSS to hide UI elements
  • Purpose: Pixel-perfect screen capture with content sidebars, minus UI chrome
  • Characteristics:
    • Shows skills sidebars (screen layout preserved)
    • No UI elements (hidden via injected CSS: .action-bar, .navigation-menu, footer, etc.)
    • Uses natural screen layout (pixel-perfect rendering)
    • ~16 pages, ~3.6 MB
    • Full digital experience without UI chrome

Why This Approach?

  1. Clean version needs print-optimized layout (compact, professional)
  2. Extended version needs pixel-perfect screen layout with sidebars visible
  3. CDP's emulation.SetEmulatedMedia() allows switching between @media print and @media screen
  4. Injected CSS surgically hides only UI elements without affecting content layout

Implementation Details:

  • RenderModePrint: Uses default @media print (hides sidebars + UI via print.css)
  • RenderModeScreen:
    1. Emulates @media screen to preserve natural layout
    2. Injects CSS to hide UI: .no-print, .action-bar, .navigation-menu, .hamburger-btn, footer, .back-to-top, .info-button, .info-modal, .error-toast, .cv-title-badges-header, .cv-footer
    3. Keeps all content and sidebars with their natural screen styling

See: internal/pdf/generator.go lines 146-165 for implementation

PDF Generation Flow

User selects PDF option in modal
    ↓
Frontend: /export/pdf?lang={lang}&length={length}&icons={icons}&version={version}
    ↓
Backend: GenerateFromURLWithOptions(url, cookies, RenderMode)
    ↓
Chromedp: Navigate to URL with cookies
    ↓
CSS Injection: Override print.css + Show/hide sidebars + Layout adjustments
    ↓
PDF Generation: PrintToPDF with A4 dimensions
    ↓
Output: PDF file (4, 5, 7, or 9 pages depending on configuration)

2-Column vs 3-Column Layout Decision

Critical Architecture Decision for long CV with sidebars:

Previous Approach (3-column - WRONG)

.page-1, .page-2: 25% | 50% | 25%  /* Always 3 columns */

Problems:

  • Page 1 had empty right column (25% wasted space)
  • Page 2 had empty left column (25% wasted space)
  • Result: 10-19 pages (excessive)

Current Approach (2-column - CORRECT)

.page-1: 25% | 75%    /* Left sidebar + Main, NO right space */
.page-2: 75% | 25%    /* Main + Right sidebar, NO left space */

Benefits:

  • No wasted space on either page
  • Matches web's actual layout exactly
  • Result: 9 pages (optimal)

Sidebar Width Configuration

Sidebar width affects page count and readability:

Width Layout Pages Notes
18% 18% | 82% 9 pages Too narrow, cramped
20% 20% | 80% 8 pages Minimal sidebars
25% 25% | 75% 9 pages Current (optimal balance)
30% 30% | 70% 10+ pages Too wide, excessive

Current Setting: 25% sidebars (configured in internal/pdf/generator.go:176,181)

To adjust sidebar width, update these lines in internal/pdf/generator.go:

// Page 1: Left sidebar (XX%) + Main (YY%) - NO right space
'.page-1 .page-content { grid-template-columns: XX% YY% !important; }' +

// Page 2: Main (YY%) + Right sidebar (XX%) - NO left space
'.page-2 .page-content { grid-template-columns: YY% XX% !important; }'

// Where XX + YY = 100

Detailed CSS Injection Strategy

For RenderModeScreen (long CV with sidebars), the following CSS is injected:

// Override parent width constraints (full A4 width)
'.cv-page, .cv-paper, .cv-container { max-width: 100% !important; width: 100% !important; }'

// Show sidebars (override print.css hiding)
'.cv-sidebar, .cv-sidebar-left, .cv-sidebar-right { display: block !important; }'

// Hide mobile UI elements (accordion headers, navigation, etc.)
'.sidebar-accordion-header { display: none !important; }'
'.no-print, .action-bar, .navigation-menu, .hamburger-btn { display: none !important; }'
'footer, .back-to-top, .info-button, .info-modal { display: none !important; }'

// Force page break before page 2
'.page-2 { page-break-before: always !important; break-before: page !important; }'

// 2-column layouts (no wasted space)
'.page-1 .page-content { grid-template-columns: 25% 75% !important; }'  // Left + Main
'.page-2 .page-content { grid-template-columns: 75% 25% !important; }'  // Main + Right

HTML Structure

Page 1 - Left sidebar + Main content:

<div class="cv-page page-1">
  <div class="page-content">
    <aside class="cv-sidebar cv-sidebar-left">
      <!-- Skills, languages, etc. -->
    </aside>
    <main class="cv-main">
      <!-- Work experience, education -->
    </main>
  </div>
</div>

Page 2 - Main content + Right sidebar:

<div class="cv-page page-2">
  <div class="page-content">
    <main class="cv-main">
      <!-- Continued work experience -->
    </main>
    <aside class="cv-sidebar cv-sidebar-right">
      <!-- Additional skills, references -->
    </aside>
  </div>
</div>

Compact Sidebar Fonts Feature

Overview: Automatically reduces sidebar font sizes by 2-6% only for short CVs with skills (length=short&version=with_skills), reducing page count from 6 to 5 pages while maintaining readability.

Impact:

  • Page count reduction: 6 → 5 pages (16.7% reduction)
  • Font size reduction: 2-6% (very subtle, 0.94-0.98em)
  • Readability: Maintained - fonts remain professional
  • Only for short version - Long version uses full-size fonts

Activation Conditions:

  1. length=short (detected via cv-length cookie)
  2. version=with_skills (RenderModeScreen with sidebars)

Does NOT activate for:

  • Long CVs (length=long) - always use full-size fonts
  • Clean version (version=clean) - no sidebars shown

Implementation (internal/pdf/generator.go lines 154-215):

// Check if this is a short version (to apply compact sidebar fonts)
isShortVersion := cookies["cv-length"] == "short"

compactFontCSS := ""
if isShortVersion {
    compactFontCSS = `
        /* Compact sidebar fonts (SHORT VERSION ONLY) */
        .cv-sidebar * { font-size: 0.96em !important; line-height: 1.4 !important; }
        .cv-sidebar h3 { font-size: 0.98em !important; margin: 0.4em 0 !important; }
        .cv-sidebar h4 { font-size: 0.96em !important; margin: 0.35em 0 !important; }
        .cv-sidebar p, .cv-sidebar li { font-size: 0.94em !important; line-height: 1.4 !important; }
        .cv-sidebar ul, .cv-sidebar ol { margin: 0.4em 0 0.4em 1.2em !important; }
        .cv-sidebar li { margin-bottom: 0.25em !important; }
        .cv-sidebar section { margin-bottom: 0.8em !important; }
    `
}

Font Size Breakdown:

Element Full-Size (Long) Compact (Short) Reduction
General text 1.0em 0.96em 4%
H3 headings 1.0em 0.98em 2%
H4 headings 1.0em 0.96em 4%
Paragraphs & lists 1.0em 0.94em 6%

Design Philosophy:

  • Readability first: 2-6% reduction is barely noticeable
  • Professional appearance: No "squeezed" or cramped feel
  • Natural flow: Content reflows organically, not forced
  • Consistent UX: Main content uses full-size fonts

Feature Specifications

Export Options

1. Short CV (Clean Version - Short)

  • Length: short (essential information only)
  • Version: clean (no skills sidebar)
  • Page Count: 4 pages
  • Use Case: Job applications requiring concise CVs
  • Parameters: ?lang={lang}&length=short&icons=show&version=clean
  • Filename: cv-short-jamr-{year}-{lang}.pdf (version omitted for clean)

2. Long CV (Extended Version - With Skills)

  • Length: extended (comprehensive information)
  • Version: with_skills (includes skills sidebar)
  • Page Count: ~16 pages (natural screen layout with sidebars preserved)
  • Use Case: Detailed applications requiring full work history with skills showcase
  • Parameters: ?lang={lang}&length=long&icons=show&version=with_skills
  • Filename: cv-long-with-skills-jamr-{year}-{lang}.pdf

3. Current View

  • Length: From localStorage (cv-length) - mapped to new naming
  • Version: From localStorage (cv-theme) - mapped to new naming
  • Icons: From localStorage (cv-icons)
  • Page Count: Variable based on settings
  • Use Case: Export exactly what's displayed on screen
  • Parameters: Dynamic based on localStorage with automatic mapping

Naming Convention - Clear and Descriptive

All exported PDFs follow a consistent, intuitive naming convention:

cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf

WHERE:
  {length}   = short | long
  {version}  = OMITTED for clean | with_skills for extended
  {initials} = User initials (e.g., "jamr")
  {year}     = Current year (2025)
  {lang}     = es | en

Key Design Decision: Version is OMITTED when it's "clean" to keep filenames concise and clear.

Filename Examples

Modal Option Settings Generated Filename
Short CV short + clean cv-short-jamr-2025-es.pdf
Long CV long + with_skills cv-long-with-skills-jamr-2025-en.pdf
Current View short + with_skills cv-short-with-skills-jamr-2025-es.pdf
Current View long + clean cv-long-jamr-2025-en.pdf

Comprehensive Combinations Matrix

Length Version Filename Pattern
short clean cv-short-jamr-{year}-{lang}.pdf
short with_skills cv-short-with-skills-jamr-{year}-{lang}.pdf
long clean cv-long-jamr-{year}-{lang}.pdf
long with_skills cv-long-with-skills-jamr-{year}-{lang}.pdf

Dynamic Features

1. Year Placeholder System

Static PDF URLs in JSON data files use a {{YEAR}} placeholder that's automatically replaced with the current year when the application loads.

JSON Configuration:

{
  "url": "https://juan.andres.morenorub.io/static/pdf/cv-short-jamr-{{YEAR}}-es.pdf"
}

Runtime Replacement:

  • Handled by LoadCV() function in internal/models/cv.go
  • Replaces {{YEAR}} with time.Now().Year()
  • Ensures URLs always reference current year's PDFs

2. Initials Extraction

Initials are dynamically generated from the user's full name:

nameParts := strings.Fields(cv.Personal.Name)
initials := ""
for _, part := range nameParts {
    if len(part) > 0 {
        initials += string([]rune(part)[0])
    }
}
initials = strings.ToLower(initials)

Example: "Juan Andrés Moreno Rubio" → "jamr"

3. Legacy LocalStorage Mapping

For backwards compatibility, the system automatically maps old localStorage values to the new naming convention:

// Old → New mapping (for backwards compatibility)
'long'  'long'
'long' (theme)  'with_skills'
// Note: 'short' now stays as 'short' (no longer maps to 'detailed')
'clean'  'clean' (unchanged)

This ensures existing users' preferences continue to work seamlessly.

Print-Friendly Design: Light Mode Only

Critical Policy

PDFs are ALWAYS generated in light mode, regardless of the user's color theme preference.

This is a fundamental design decision for print-friendliness and readability:

  • Light backgrounds with dark text for optimal printing
  • Professional appearance in any context
  • Reduced printer ink consumption
  • Consistent output across all use cases
  • Dark mode is NEVER used for PDF generation

Multi-Layer Enforcement

The system enforces light mode at THREE levels to guarantee print-friendly PDFs:

File: internal/handlers/cv.go

The PDF export handler ALWAYS sets the color-theme cookie to "light":

// CRITICAL: ALWAYS force light mode for PDF generation (print-friendly)
// This ensures PDFs are NEVER generated in dark mode, regardless of user's preference
cookies["color-theme"] = "light"

This ensures the browser context used for PDF generation starts with light mode enabled.

2. CSS Print Media Query Override

File: static/css/08-contexts/_print.css

The print stylesheet forcibly overrides ALL color theme CSS variables:

@media print {
    /* CRITICAL: FORCE LIGHT MODE FOR ALL PDFs */
    /* PDF generation MUST ALWAYS use light mode colors */
    /* This overrides ANY color theme (dark/auto/light) */
    *,
    :root,
    [data-color-theme="dark"],
    [data-color-theme="auto"],
    [data-color-theme="light"],
    html,
    body {
        --page-bg: #b8bbbe !important;
        --paper-bg: #ffffff !important;
        --text-primary: #1a1a1a !important;
        --text-secondary: #333333 !important;
        /* ... all light mode variables ... */
    }
}

This ensures that even if JavaScript fails or cookies don't propagate, the CSS will force light mode colors during print/PDF generation.

3. Print Color Accuracy

The print stylesheet also includes critical browser directives:

@media print {
    * {
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
        color-adjust: exact !important;
    }
}

This ensures browsers render colors exactly as specified, preventing automatic adjustments that could affect print quality.

Why Three Layers?

This defense-in-depth approach guarantees light mode PDFs even if:

  • Cookies fail to set or propagate
  • JavaScript doesn't execute properly
  • Browser preferences override theme settings
  • CSS cascade issues occur

Result: Bulletproof light mode enforcement for all PDF exports.

Technical Implementation

File Structure

/Users/txeo/Git/yo/cv/
├── internal/
│   ├── handlers/
│   │   └── cv.go                    # PDF export handler with filename generation
│   └── models/
│       └── cv.go                    # CV data loading with year placeholder replacement
├── templates/
│   └── partials/
│       └── modals/
│           └── pdf-modal.html       # Interactive PDF download modal
├── static/
│   ├── css/
│   │   └── 04-interactive/
│   │       └── _remaining.css       # Modal styling (red theme for PDF)
│   └── pdf/
│       ├── cv-short-jamr-2025-es.pdf
│       ├── cv-short-jamr-2025-en.pdf
│       ├── cv-long-with-skills-jamr-2025-es.pdf
│       └── cv-long-with-skills-jamr-2025-en.pdf
├── data/
│   ├── cv-es.json                   # Spanish CV data with {{YEAR}} placeholders
│   └── cv-en.json                   # English CV data with {{YEAR}} placeholders
└── tests/
    └── mjs/
        ├── 14-pdf-modal.test.mjs           # Modal UI and interaction tests
        └── 24-pdf-download-params.test.mjs # Parameter validation tests

Backend: Parameter Validation and Filename Generation

File: internal/handlers/cv.go

Parameter Validation:

// Length parameter: "short" or "long"
length := r.URL.Query().Get("length")
if length == "" {
    length = "short"
}
if length != "short" && length != "long" {
    HandleError(w, r, BadRequestError("Unsupported length. Use 'short' or 'long'"))
    return
}

// Version parameter: "clean" or "with_skills"
version := r.URL.Query().Get("version")
if version == "" {
    version = "with_skills"
}
if version != "with_skills" && version != "clean" {
    HandleError(w, r, BadRequestError("Unsupported version. Use 'with_skills' or 'clean'"))
    return
}

Filename Generation:

// Generate initials from name
nameParts := strings.Fields(cv.Personal.Name)
initials := ""
for _, part := range nameParts {
    if len(part) > 0 {
        initials += string([]rune(part)[0])
    }
}
initials = strings.ToLower(initials)

// Get current year
currentYear := time.Now().Year()

// Build filename: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf
// Omit version if it's "clean"
var filename string
if version == "clean" {
    filename = fmt.Sprintf("cv-%s-%s-%d-%s.pdf", length, initials, currentYear, lang)
} else {
    filename = fmt.Sprintf("cv-%s-%s-%s-%d-%s.pdf", length, version, initials, currentYear, lang)
}

Examples:

  • short + clean → cv-short-jamr-2025-es.pdf
  • long + with_skills → cv-long-with-skills-jamr-2025-en.pdf

Frontend: Modal Interaction with Legacy Mapping

File: templates/partials/modals/pdf-modal.html

function downloadPDF() {
    const selectedCard = document.querySelector('#pdf-modal .pdf-option-card.selected');
    const selectedFormat = selectedCard.getAttribute('data-cv-format');
    const lang = '{{.Lang}}';
    let url;

    if (selectedFormat === 'short') {
        // Short CV: clean version (no skills), short length
        url = `/export/pdf?lang=${lang}&length=short&icons=show&version=clean`;
    } else if (selectedFormat === 'long') {
        // Long CV: with skills sidebar, extended length
        url = `/export/pdf?lang=${lang}&length=long&icons=show&version=with_skills`;
    } else if (selectedFormat === 'current') {
        // Current view: use localStorage settings with mapping
        let currentLength = localStorage.getItem('cv-length') || 'short';

        // Map old values to new naming convention
        if (currentLength === 'long') currentLength = 'long';
        // 'short' stays as 'short' - no mapping needed

        const currentIcons = localStorage.getItem('cv-icons') || 'show';
        const currentTheme = localStorage.getItem('cv-theme') || 'default';
        const version = currentTheme === 'clean' ? 'clean' : 'with_skills';

        url = `/export/pdf?lang=${lang}&length=${currentLength}&icons=${currentIcons}&version=${version}`;
    }

    window.location.href = url;
}

Testing

Test Suite

1. Modal UI Test: 14-pdf-modal.test.mjs

Tests the modal interface and user interactions:

  • Modal structure (3 thumbnail cards, download button)
  • Card selection behavior (radio button pattern)
  • Download button enable/disable logic
  • Keyboard navigation (Tab, Enter, Space, ESC)
  • Accessibility (ARIA attributes, screen reader support)
  • Responsive layout (mobile/tablet/desktop)
  • Multilingual support (EN/ES)

2. Parameter Validation Test: 24-pdf-download-params.test.mjs

Tests PDF export parameters and filename generation:

  • Short CV parameters: length=short&version=clean
  • Long CV parameters: length=long&version=with_skills
  • Current View parameters: reads from localStorage with mapping
  • Filename format: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf
  • Version omitted for clean
  • Dynamic year generation
  • Legacy value mapping (long→extended)

Run Tests:

# Run modal UI tests
bun tests/mjs/14-pdf-modal.test.mjs

# Run parameter validation tests
bun tests/mjs/24-pdf-download-params.test.mjs

# Run all tests
bun tests/run-all.mjs

Manual Testing Checklist

  • Open PDF modal from download button
  • Select each of the three options
  • Verify download button enables after selection
  • Click download button for each option
  • Verify correct filename generated (check version omission for clean)
  • Check PDF opens correctly
  • Test in Spanish and English
  • Test on mobile/tablet/desktop
  • Verify keyboard navigation (Tab, Enter, ESC)
  • Test with different localStorage settings for Current View
  • Verify legacy localStorage values map correctly

API Endpoint

/export/pdf

Method: GET

Parameters:

  • lang (optional, default: "en"): es or en
  • length (optional, default: "short"): short or extended
  • icons (optional, default: "show"): show or hide
  • version (optional, default: "with_skills"): clean or with_skills

Response:

  • Success: PDF file with appropriate filename and Content-Disposition header
  • Rate Limited: 429 Too Many Requests with "Rate limit exceeded" message
  • Error: 400 Bad Request for invalid parameters, 500 Internal Server Error for generation failures

Example Requests:

# Short clean CV in Spanish
curl -O http://localhost:1999/export/pdf?lang=es&length=short&icons=show&version=clean
# Filename: cv-short-jamr-2025-es.pdf

# Extended with skills CV in English
curl -O http://localhost:1999/export/pdf?lang=en&length=long&icons=show&version=with_skills
# Filename: cv-long-with-skills-jamr-2025-en.pdf

Shortcut URLs (Year-Aware)

Overview

The application provides memorable shortcut URLs for the default CV (short with skills, 5 pages) that are easy to share and remember.

Pattern: /cv-jamr-{year}-{lang}.pdf

Examples:

  • https://juan.andres.morenorub.io/cv-jamr-2025-en.pdf (English)
  • https://juan.andres.morenorub.io/cv-jamr-2025-es.pdf (Spanish)

Year Validation

The shortcut URLs include automatic year validation that auto-updates annually:

Current Year (2025):

  • /cv-jamr-2025-en.pdf → Works (301 redirect)
  • /cv-jamr-2025-es.pdf → Works (301 redirect)

Past/Future Years:

  • /cv-jamr-2024-en.pdf → 404 Not Found
  • /cv-jamr-2026-en.pdf → 404 Not Found (until 2026)

Auto-Update: Next year (2026), the 2026 URLs will automatically work and 2025 URLs will return 404.

Implementation

Backend Handler (internal/handlers/cv.go):

func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) {
    // Extract year and language from URL path
    path := r.URL.Path  // e.g., "/cv-jamr-2025-en.pdf"
    parts := strings.Split(strings.TrimPrefix(path, "/"), "-")

    yearStr := parts[2]
    lang := strings.TrimSuffix(parts[3], ".pdf")

    // Validate year matches current year
    currentYear := fmt.Sprintf("%d", time.Now().Year())
    if yearStr != currentYear {
        http.NotFound(w, r)
        return
    }

    // Validate language
    if lang != "en" && lang != "es" {
        http.NotFound(w, r)
        return
    }

    // Redirect to default PDF (short with skills)
    redirectURL := fmt.Sprintf("/export/pdf?lang=%s&length=short&icons=show&version=with_skills", lang)
    http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
}

Route Registration (internal/routes/routes.go):

// Shortcut routes - must be before "/" route for precedence
mux.HandleFunc("/cv-jamr-", cvHandler.DefaultCVShortcut)

Frontend Integration (templates/partials/modals/pdf-modal.html):

if (selectedFormat === 'default') {
    // Default CV: use shortcut URL
    const currentYear = new Date().getFullYear();
    url = `/cv-jamr-${currentYear}-${lang}.pdf`;
}

Benefits

For Users:

  • Memorable URL pattern
  • Easy to type and share
  • No complex query parameters
  • Professional appearance

For Developers:

  • Auto-updates yearly (no manual changes)
  • Simple validation logic
  • SEO-friendly clean URLs
  • Minimal maintenance overhead

Testing

# Test current year (should work)
curl -I http://localhost:1999/cv-jamr-2025-en.pdf
# Expected: HTTP/1.1 301 Moved Permanently
# Location: /export/pdf?lang=en&length=short&icons=show&version=with_skills

# Test invalid year (should fail)
curl -I http://localhost:1999/cv-jamr-2024-en.pdf
# Expected: HTTP/1.1 404 Not Found

# Run automated test
bun tests/mjs/28-references-pdf-download.test.mjs

References Section Integration

The shortcut URLs are integrated into the References section of the CV for easy access:

Template Integration (templates/partials/sections/references.html):

{{if eq .Action "downloadPDF"}}
  <a href="/cv-jamr-{{$.CurrentYear}}-{{$.Lang}}.pdf"
     target="_blank"
     rel="noopener noreferrer">
    <strong>{{.LinkText}}</strong>
  </a>
{{end}}

Data Configuration (data/cv-en.json, data/cv-es.json):

{
  "title": "Download this curriculum in English",
  "action": "downloadPDF",
  "textBefore": "Download this curriculum in",
  "linkText": "English"
}

Key Features:

  • Direct link - No modal, no JavaScript dependency
  • Language-aware - Spanish CV gets Spanish link, English CV gets English link
  • Year-aware - Automatically uses current year
  • Works everywhere - PDF exports, emails, external shares
  • Security - Proper target="_blank" and rel="noopener noreferrer" attributes

Use Cases:

  • Works in printed PDFs (link remains functional)
  • Works in email clients (direct download link)
  • Works without JavaScript (static HTML link)
  • Works when shared externally (permanent, memorable URL)

Design Philosophy

Why This Naming Convention?

  1. Clarity: "short" and "long" clearly communicate content depth
  2. Simplicity: Version omitted for clean keeps filenames concise
  3. Consistency: All components follow the same pattern
  4. Intuitive: Non-technical users can understand what each filename means
  5. Professional: Matches industry standards for document naming

Old vs New Comparison

Old Naming New Naming Improvement
cv-detailed-jamr-2025-es.pdf (v1) cv-short-jamr-2025-es.pdf Simpler, more intuitive
cv-long-extended-en-jamr-2025.pdf cv-long-with-skills-jamr-2025-en.pdf More descriptive, better clarity
Language before year Language after year Better organization

Maintenance

Updating Static PDFs

To regenerate static PDFs referenced in JSON files:

# Short + clean (version omitted)
curl -o static/pdf/cv-short-jamr-2025-es.pdf \
  "http://localhost:1999/export/pdf?lang=es&length=short&icons=show&version=clean"

curl -o static/pdf/cv-short-jamr-2025-en.pdf \
  "http://localhost:1999/export/pdf?lang=en&length=short&icons=show&version=clean"

# Extended + with_skills
curl -o static/pdf/cv-long-with-skills-jamr-2025-es.pdf \
  "http://localhost:1999/export/pdf?lang=es&length=long&icons=show&version=with_skills"

curl -o static/pdf/cv-long-with-skills-jamr-2025-en.pdf \
  "http://localhost:1999/export/pdf?lang=en&length=long&icons=show&version=with_skills"

Year Rollover

The system automatically handles year rollovers:

  • Filename Generation: Uses time.Now().Year() at runtime
  • URL Placeholders: {{YEAR}} replaced during data load
  • No Manual Updates Required: All year references are dynamic

Troubleshooting

Wrong Filename Format

Symptom: Filename doesn't match cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf

Solutions:

  1. Verify backend logic in internal/handlers/cv.go lines 306-313
  2. Check version is correctly omitted for clean
  3. Ensure language appears at the end
  4. Verify year extraction logic

Legacy localStorage Not Mapping

Symptom: Old localStorage values (short, long) not working in Current View

Solutions:

  1. Check JavaScript mapping logic in pdf-modal.html lines 257-260
  2. Verify mapping: shortdetailed, longextended
  3. Test with console.log to see actual values
  4. Clear localStorage and test with fresh values

Parameter Validation Errors

Symptom: 400 Bad Request when downloading PDF

Solutions:

  1. Check allowed values: length ∈ {short, extended}, version ∈ {clean, with_skills}
  2. Verify frontend sends correct parameters
  3. Check browser network tab for actual request
  4. Run parameter validation test: bun tests/mjs/24-pdf-download-params.test.mjs

PDF Generated in Dark Mode

Symptom: Downloaded PDF has dark background and light text

Root Cause: Light mode enforcement not working at one or more layers

Solutions:

  1. Verify Backend Cookie Setting (internal/handlers/cv.go:260-270):

    // Ensure this line exists:
    cookies["color-theme"] = "light"
    
    • Check server logs for cookie setting
    • Restart server if code was recently updated
  2. Verify CSS Print Overrides (static/css/08-contexts/_print.css:4-60):

    @media print {
        /* Verify this section exists at the top */
        *,
        :root,
        [data-color-theme="dark"],
        [data-color-theme="auto"] {
            --paper-bg: #ffffff !important;
            --text-primary: #1a1a1a !important;
            /* ... all light mode variables ... */
        }
    }
    
    • Clear browser cache
    • Check CSS file is being served correctly
  3. Test with Browser Dev Tools:

    • Open page in browser
    • Enable dark mode
    • Open Print Preview (Cmd/Ctrl+P)
    • Verify preview shows light background
    • If preview is dark, CSS overrides aren't working
  4. Manual Testing:

    # Set user to dark mode
    # Then generate PDF
    curl -O "http://localhost:1999/export/pdf?lang=es&length=short&version=clean"
    
    # Open PDF and verify it has white background
    

Expected Result: PDF ALWAYS has white background (#ffffff) and dark text (#1a1a1a), regardless of user's color theme preference.


Changelog

Version 2.0.0 (2025-11-19) - New Naming Convention & Light Mode Enforcement

🎯 Major Changes:

1. Complete Naming Convention Overhaul

  • BREAKING: Changed from short/long to short/extended for better clarity
  • BREAKING: Changed from extended (theme) to with_skills for better clarity
  • NEW: Version omitted from filename when clean (no skills sidebar)
  • NEW: Language moved to end: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf
  • UPDATE: Originally used detailed/extended, changed to short/extended for better contrast

Old Naming Examples:

  • cv-short-clean-es-jamr-2025.pdf (v0)
  • cv-long-extended-en-jamr-2025.pdf (v0)

New Naming Examples:

  • cv-short-jamr-2025-es.pdf (version omitted, final naming)
  • cv-long-with-skills-jamr-2025-en.pdf (version included)

2. Light Mode Enforcement - Defense in Depth

CRITICAL: PDFs are now GUARANTEED to always use light mode, regardless of user's color theme preference.

Three-Layer Protection:

  1. Backend Cookie Enforcement (internal/handlers/cv.go:262-264)

    • Forces color-theme=light cookie for all PDF generation requests
    • Ensures browser context starts in light mode
  2. CSS Print Media Query Override (static/css/08-contexts/_print.css:4-60)

    • Forcibly overrides ALL CSS variables to light mode values
    • Uses !important to override any theme settings
    • Applies to :root, [data-color-theme="dark"], [data-color-theme="auto"]
  3. Browser Print Directives (static/css/08-contexts/_print.css:65-69)

    • print-color-adjust: exact !important
    • Ensures browsers render colors exactly as specified

Why Three Layers?

  • Guarantees light mode even if cookies fail, JavaScript doesn't execute, or browser settings override
  • Provides optimal print quality and reduced ink consumption
  • Professional appearance in all contexts

3. Backend Improvements

  • Added: time package import to internal/models/cv.go for year placeholder system
  • Updated: Parameter validation for new naming (detailed/extended, clean/with_skills)
  • Updated: Filename generation logic with conditional version inclusion

4. Frontend Updates

  • Updated: PDF modal JavaScript to use new parameters
  • Added: Legacy localStorage mapping for backwards compatibility
    • longextended
    • extended (theme) → with_skills
    • Note: short now stays as short (no longer maps to detailed)

5. Static Assets

  • Generated: 4 new PDFs with correct naming convention (2.2 MB each)
    • cv-short-jamr-2025-es.pdf (updated from cv-detailed)
    • cv-short-jamr-2025-en.pdf (updated from cv-detailed)
    • cv-long-with-skills-jamr-2025-es.pdf
    • cv-long-with-skills-jamr-2025-en.pdf
  • Removed: Old PDFs with deprecated naming (cv-detailed-*)

6. Documentation

  • NEW: "Print-Friendly Design: Light Mode Only" section
  • NEW: Comprehensive multi-layer enforcement explanation
  • NEW: Troubleshooting section for dark mode PDF issues
  • Updated: All examples to use new naming convention
  • Updated: Filename combinations matrix
  • Updated: Design philosophy section

7. Tests

  • Updated: 24-pdf-download-params.test.mjs with new parameter expectations
  • Note: Test needs adjustments to handle PDF downloads (doesn't navigate page)

Migration Path for Existing Users:

  • Old localStorage values automatically map to new naming
  • No user action required - seamless transition
  • Old PDFs can be regenerated with new naming using provided curl commands

Benefits:

  • Clearer, more intuitive naming convention
  • More professional filename format
  • Bulletproof light mode enforcement
  • Better print quality and ink efficiency
  • Backwards compatible with existing user preferences
  • Comprehensive documentation and troubleshooting

Version 2.1.0 (2025-11-20) - Year-Aware Shortcut URLs

🎯 New Features:

Shortcut URLs for Default CV

  • NEW: Memorable shortcut pattern /cv-jamr-{year}-{lang}.pdf
  • NEW: Automatic year validation (only current year accepted)
  • NEW: Auto-updates yearly without code changes
  • NEW: "Default CV (Recommended)" option in PDF modal
  • Benefits:
    • Easy to remember and share: juan.andres.morenorub.io/cv-jamr-2025-en.pdf
    • No complex query parameters
    • Professional, clean URLs
    • 301 redirect to proper PDF export endpoint

Implementation:

  • Handler: DefaultCVShortcut() in internal/handlers/cv.go:222-263
  • Route: Registered in internal/routes/routes.go:17
  • Modal: "Default CV" option with visual highlighting (purple gradient, star emoji)
  • Frontend: Dynamic year detection via JavaScript

Testing:

  • Current year URLs (2025): 301 redirect to default PDF
  • Past/future years: 404 Not Found
  • Both languages supported (en, es)

Last Updated: 2025-11-20 Version: 2.1.0 (Year-Aware Shortcut URLs) Status: Production Maintainer: Development Team