refactor: Rename 'extended' → 'long' + add compact sidebar fonts

BREAKING CHANGE: API parameter renamed from 'extended' to 'long'

## Breaking Change: Terminology Standardization

Renamed 'extended' to 'long' across entire codebase for consistency:

**Backend (Go):**
- internal/handlers/cv.go (7 locations)
  - Migration logic to auto-convert 'extended' → 'long' cookies
  - API validation now rejects 'extended', requires 'long'
  - Toggle state logic updated
- internal/handlers/pdf_test.go (17 occurrences)
  - Test function renamed: TestExportPDF_ExtendedWithSkills → TestExportPDF_LongWithSkills
  - All test cases, parameters, and expected filenames updated
- internal/pdf/generator.go (2 comment updates)

**Frontend:**
- PDF-EXPORT-FEATURE.md (3 occurrences)
- doc/3-API.md (parameter documentation)
- doc/7-CUSTOMIZATION.md (examples updated)
- templates/partials/modals/pdf-modal.html (button text, URLs)
- static/js/main.js (migration logic)
- static/hyperscript/toggles._hs (toggle logic)
- tests/mjs/24-pdf-download-params.test.mjs (test expectations)
- tests/mjs/test-preference-migration.test.mjs (NEW)
- tests/mjs/verify-migration.test.mjs (NEW)

**PDFs Renamed:**
- cv-extended-with_skills-jamr-2025-en.pdf → cv-long-with_skills-jamr-2025-en.pdf
- cv-extended-with_skills-jamr-2025-es.pdf → cv-long-with_skills-jamr-2025-es.pdf

**Migration:** Automatic cookie migration from 'extended' → 'long' for seamless UX

## New Feature: Compact Sidebar Fonts

Reduces page count for short CV with skills from 6 → 5 pages:

**Implementation:**
- Location: internal/pdf/generator.go (lines 154-215)
- Cookie detection: `cookies["cv-length"] == "short"`
- Font reduction: 2-6% (0.94-0.98em) - very subtle
- Only activates for: `length=short` + `version=with_skills`
- Long version: Always uses full-size fonts

**Impact:**
- Page count: 6 pages → 5 pages (16.7% reduction)
- Readability: Maintained - fonts remain professional
- Design philosophy: Subtle, natural content flow

**Testing:**
- New test: TestPDFGenerator_CompactSidebarFonts
- Comprehensive coverage of cookie detection and PDF generation
- Manual verification: 5-page PDF with compact but readable fonts

**Documentation:**
- doc/LONG-PDF-GENERATION.md (NEW, 13 KB)
  - Complete feature documentation
  - Implementation details with code examples
  - Font size breakdown table
  - Testing and troubleshooting guides
  - Compact sidebar fonts section (comprehensive)

**Files Changed:**
- 11 modified (backend + frontend + docs)
- 5 new files (2 PDFs, 1 doc, 2 tests)
- 2 files renamed (PDFs)

**Tests:** All Go tests passing, API validation verified, PDF generation tested
This commit is contained in:
juanatsap
2025-11-20 11:21:43 +00:00
parent 925a95c1b4
commit b44f9b9a99
18 changed files with 1262 additions and 80 deletions
+114 -15
View File
@@ -78,8 +78,23 @@ func (g *Generator) GenerateFromURL(ctx context.Context, url string) ([]byte, er
return pdfBuffer, nil
}
// RenderMode determines how the PDF is rendered
type RenderMode string
const (
// RenderModePrint uses @media print CSS (clean, print-friendly)
RenderModePrint RenderMode = "print"
// RenderModeScreen uses @media screen CSS (long, full page with sidebars)
RenderModeScreen RenderMode = "screen"
)
// GenerateFromURLWithCookies generates a PDF from a given URL with custom cookies
func (g *Generator) GenerateFromURLWithCookies(ctx context.Context, url string, cookies map[string]string) ([]byte, error) {
return g.GenerateFromURLWithOptions(ctx, url, cookies, RenderModePrint)
}
// GenerateFromURLWithOptions generates a PDF with custom cookies and render mode
func (g *Generator) GenerateFromURLWithOptions(ctx context.Context, url string, cookies map[string]string, mode RenderMode) ([]byte, error) {
// Create context with timeout
ctx, cancel := context.WithTimeout(ctx, g.timeout)
defer cancel()
@@ -125,23 +140,107 @@ func (g *Generator) GenerateFromURLWithCookies(ctx context.Context, url string,
chromedp.WaitReady("body"),
// Small delay to ensure all content is loaded
chromedp.Sleep(500*time.Millisecond),
// Generate PDF with print-optimized settings
chromedp.ActionFunc(func(ctx context.Context) error {
var err error
pdfBuffer, _, err = page.PrintToPDF().
WithPrintBackground(true).
WithPreferCSSPageSize(true).
WithMarginTop(0).
WithMarginBottom(0).
WithMarginLeft(0).
WithMarginRight(0).
WithPaperWidth(8.27). // A4 width in inches
WithPaperHeight(11.69). // A4 height in inches
Do(ctx)
return err
}),
)
// Apply mode-specific customizations
if mode == RenderModeScreen {
// For long version: Use print media for compact layout, but show sidebars
// Print CSS hides sidebars - we override that to show them
// UI elements remain hidden (already handled by print CSS)
// Wait for page to fully render
tasks = append(tasks, chromedp.Sleep(500*time.Millisecond))
// Check if this is a short version (to apply compact sidebar fonts)
// The length parameter is passed as a cookie, not in the URL
isShortVersion := cookies["cv-length"] == "short"
// Inject CSS to show sidebars AND restore their positioning
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
// Base CSS for all versions with sidebars
baseSidebarCSS := `
(function() {
const style = document.createElement('style');
style.textContent = '@media print { ' +
// Override all width constraints for full page width
'.cv-page, .cv-paper, .cv-container { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; transform: none !important; } ' +
// Override print.css blocking display
'.cv-sidebar, .cv-sidebar-left, .cv-sidebar-right { display: block !important; } ' +
// Hide accordion header (web mobile UI element)
'.sidebar-accordion-header { display: none !important; } ' +
// Force page break before page 2 (start right sidebar on new page)
'.page-2 { page-break-before: always !important; break-before: page !important; } ' +
// Grid Layout - Match web's 2-column approach (no wasted space!)
'.page-content { ' +
'display: grid !important; ' +
'gap: 0 !important; ' +
'width: 100% !important; ' +
'max-width: 100% !important; ' +
'} ' +
// Page 1: Left sidebar (25%) + Main (75%) - NO right space
'.page-1 .page-content { ' +
'grid-template-columns: 25% 75% !important; ' +
'} ' +
// Page 2: Main (75%) + Right sidebar (25%) - NO left space
'.page-2 .page-content { ' +
'grid-template-columns: 75% 25% !important; ' +
'} ' +
// Sidebar positioning and padding
'.cv-sidebar-left { grid-column: 1 !important; padding: 12mm 8mm !important; } ' +
'.cv-sidebar-right { grid-column: 2 !important; padding: 12mm 8mm !important; } ' +
// Main content positioning (different column for each page)
'.page-1 .cv-main { grid-column: 2 !important; max-width: none !important; width: 100% !important; padding: 12mm 10mm !important; } ' +
'.page-2 .cv-main { grid-column: 1 !important; max-width: none !important; width: 100% !important; padding: 12mm 10mm !important; } '`
// Add compact font styles ONLY for short version
compactFontCSS := ""
if isShortVersion {
compactFontCSS = ` +
// Compact sidebar fonts (SHORT VERSION ONLY) - very subtle reduction to let content flow naturally
'.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; padding: 0 !important; } ' +
'.cv-sidebar h4 { font-size: 0.96em !important; margin: 0.35em 0 !important; padding: 0 !important; } ' +
'.cv-sidebar p, .cv-sidebar li { font-size: 0.94em !important; line-height: 1.4 !important; margin: 0.3em 0 !important; padding: 0 !important; } ' +
'.cv-sidebar ul, .cv-sidebar ol { margin: 0.4em 0 0.4em 1.2em !important; padding: 0 !important; } ' +
'.cv-sidebar li { margin-bottom: 0.25em !important; } ' +
'.cv-sidebar section { margin-bottom: 0.8em !important; } '`
}
showSidebarsScript := baseSidebarCSS + compactFontCSS + ` +
'}';
document.head.appendChild(style);
})();
`
return chromedp.Evaluate(showSidebarsScript, nil).Do(ctx)
}))
}
// For RenderModePrint (clean version): use default print media (@media print CSS)
// which hides both UI elements and sidebars
// Generate PDF
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
var err error
pdfBuffer, _, err = page.PrintToPDF().
WithPrintBackground(true).
WithPreferCSSPageSize(true).
WithMarginTop(0).
WithMarginBottom(0).
WithMarginLeft(0).
WithMarginRight(0).
WithPaperWidth(8.27). // A4 width in inches
WithPaperHeight(11.69). // A4 height in inches
Do(ctx)
return err
}))
// Run chromedp tasks
err := chromedp.Run(allocCtx, tasks)
if err != nil {