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:
+114
-15
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user