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
45 KiB
Customization Guide
Note: This is my personal CV website. While the code is open-source (MIT license), this guide documents my own customizations. The site is subject to modifications without notice, and I don't intend for others to use this as a template - it's publicly available code, but it's designed for my personal use.
Table of Contents
- Introduction
- Prerequisites
- Quick Customization
- Content Customization
- Visual Customization
- Template Customization
- Analytics Configuration
- Advanced Customization
- Testing Your Changes
- Examples
Introduction
This CV/Resume application is designed to be easily customizable. You can adapt it for your own CV by modifying JSON files, templates, and styles without deep Go programming knowledge.
Architecture Overview:
- Data: JSON files (
data/cv-en.json,data/cv-es.json) - Models: Go structs (
internal/models/cv.go) define data structure - Templates: Go HTML templates (
templates/*.html) render the CV - Styles: CSS (
static/css/main.css) controls appearance - Assets: Images, fonts (
static/directory)
Customization Levels:
- Basic: Edit JSON files only (name, experience, skills)
- Intermediate: Modify CSS styles and add images
- Advanced: Change templates and Go models
Prerequisites
Knowledge Requirements
- Basic: JSON syntax, file editing
- Intermediate: HTML/CSS, command-line basics
- Advanced: Go templates, basic Go programming
Tools Needed
- Text editor: VS Code, Sublime Text, or any editor
- Go 1.25.1+: For building and testing (see 8-DEPLOYMENT.md)
- Git: For version control (optional but recommended)
- Browser: For testing (Chrome/Firefox recommended)
Optional Tools
- JSON validator: JSONLint
- Image editor: For logo/photo preparation
- Make: For using Makefile commands
Quick Customization
Get started in 5 minutes:
# 1. Clone or download the project
git clone https://github.com/juanatsap/cv-site.git my-cv
cd my-cv
# 2. Edit your information
nano data/cv-en.json # or use your favorite editor
# 3. Replace your photo
cp ~/my-photo.jpg static/images/profile/dni.jpeg
# 4. Test locally
make dev
# 5. Open browser
open http://localhost:1999
Minimal changes to make it yours:
- Replace
personalsection indata/cv-en.json - Replace
summarysection - Replace
experiencesection with your jobs - Replace
educationsection - Update
skillssection - Replace profile photo
- Configure or remove analytics (see Analytics Configuration below)
Content Customization
Personal Information
Edit data/cv-en.json (and data/cv-es.json for Spanish):
Location: Top of JSON file, personal object
{
"personal": {
"name": "Your Full Name",
"title": "Your Professional Title",
"location": "Your City, Country",
"email": "your.email@example.com",
"phone": "+1 234 567 8900",
"dateOfBirth": "1990-01-01",
"placeOfBirth": "Your Birthplace",
"citizenship": "Your Nationality",
"linkedin": "https://www.linkedin.com/in/your-profile",
"github": "https://github.com/yourusername",
"domestika": "https://www.domestika.org/en/yourusername",
"website": "https://yourwebsite.com",
"photo": "/static/images/profile.jpg"
}
}
Field Descriptions:
name: Full name (displayed prominently)title: Job title or professional taglinelocation: Current locationemail: Contact email (clickable in CV)phone: Phone number with country codedateOfBirth: Birth date (YYYY-MM-DD format)placeOfBirth: Birthplacecitizenship: Nationality/citizenshiplinkedin,github,domestika,website: Social/professional linksphoto: Path to profile photo (relative to project root)
Tips:
- Use consistent formatting across English and Spanish versions
- Keep URLs absolute (include
https://) - Use international phone format (+XX XXX XXX XXXX)
- Photo should be square (400x400px minimum) for best results
JSON Schema Explained
The CV data follows a structured schema. Each section has specific fields.
Summary Section
{
"summary": "Your professional summary. 2-3 sentences highlighting your expertise, experience, and career goals. This appears prominently at the top of your CV."
}
Tips:
- Keep it concise (100-150 words)
- Highlight key achievements and expertise
- Tailor to your target audience
Experience Section
Structure:
{
"experience": [
{
"position": "Job Title",
"company": "Company Name",
"companyURL": "https://company.com", // Optional
"companyLogo": "company-logo.png", // Optional, in static/images/companies/
"location": "City, Country",
"startDate": "2020-01", // YYYY-MM format
"endDate": "2023-06", // Or "present"
"current": false, // true if still working here
"expired": false, // Optional, true if company closed
"shortDescription": "Brief one-line summary for compact view",
"responsibilities": [
"Responsibility or achievement 1",
"Responsibility or achievement 2",
"Use bullet points for clarity"
],
"technologies": [
"Technology 1",
"Technology 2",
"List relevant tech stack"
],
"highlights": [ // Optional
"Major achievement 1",
"Major achievement 2"
]
}
]
}
Field Details:
position: Job titlecompany: Company namecompanyURL: Company website (optional, makes company name clickable)companyLogo: Logo filename (place instatic/images/companies/)location: Office locationstartDate: Start date (YYYY-MM format)endDate: End date or"present"for current jobcurrent: Boolean,trueif still employedexpired: Boolean,trueif company no longer exists (grays out logo)shortDescription: One-liner for short CV version (HTML allowed)responsibilities: Array of bullet points (HTML allowed)technologies: Array of technologies usedhighlights: Optional array of major achievements
HTML in Descriptions: You can use HTML tags:
"shortDescription": "Led development of <a href='https://example.com' target='_blank' rel='noopener noreferrer'>major platform</a> serving 1M+ users."
Adding Company Logos:
- Place logo in
static/images/companies/ - Reference filename in
companyLogofield - Recommended size: 100x100px, PNG with transparency
- Fallback icon appears if logo missing
Education Section
{
"education": [
{
"degree": "Bachelor's Degree in Computer Science",
"institution": "University Name",
"location": "City, Country",
"startDate": "2015-09",
"endDate": "2019-06",
"field": "Computer Science and Engineering"
}
]
}
Tips:
- List highest degree first
- Include relevant coursework in degree name if needed
- Use
fieldfor specialization
Skills Section
Two types: Technical skills (with sidebar placement) and soft skills
{
"skills": {
"technical": [
{
"category": "Programming Languages",
"proficiency": 5, // 1-5 scale (not displayed, for internal use)
"sidebar": "left", // "left" or "right" or omit for main content
"items": [
"JavaScript (ES6+)",
"Python",
"Go"
]
},
{
"category": "Frontend Technologies",
"proficiency": 5,
"sidebar": "left",
"items": [
"React",
"HTMX",
"CSS3"
]
}
],
"soft_skills": [
"Leadership & Team Management",
"Problem-Solving",
"Communication"
]
}
}
Sidebar Layout:
- Left sidebar: Skills displayed on page 1 left side
- Right sidebar: Skills displayed on page 2 right side
- No sidebar: Skills displayed in main content area
Organizing Skills:
- Group by category (e.g., "Programming Languages", "Databases")
- Order by importance (most important categories first)
- Use specific names (e.g., "PostgreSQL" not just "SQL")
- Balance left/right sidebars for visual symmetry
Projects Section
{
"projects": [
{
"title": "Full Project Title", // Used if no projectName
"projectName": "Project Name", // Optional: linkable part
"projectDesc": "Project Description", // Optional: non-linkable part
"url": "https://project.com", // Optional
"projectLogo": "project-logo.png", // Optional, in static/images/projects/
"gitRepoUrl": "/path/to/local/repo", // Optional: for dynamic dates
"location": "City or 'Online'",
"startDate": "2023", // Optional: YYYY or YYYY-MM
"current": true, // true if ongoing
"maintainedBy": "Company Name", // Optional: if transferred
"technologies": [
"Tech 1",
"Tech 2"
],
"shortDescription": "One-line project summary",
"responsibilities": [
"What you built or contributed",
"Use bullet points"
]
}
]
}
Dynamic Dates (Advanced):
- Use
gitRepoUrlto point to a local git repository - Application will extract first commit date as start date
- Useful for open-source projects where you want automatic dating
Project Logos:
- Place in
static/images/projects/ - Recommended: 80x80px, PNG with transparency
Languages Section
{
"languages": [
{
"language": "English",
"proficiency": "Native",
"level": 5, // 1-5 scale (not displayed)
"detail": "Oral (Advanced) Written (Advanced)" // Optional
},
{
"language": "Spanish",
"proficiency": "Professional Working Proficiency",
"level": 4
}
]
}
Proficiency Levels (suggested):
- Native
- Bilingual/Fluent
- Professional Working Proficiency
- Limited Working Proficiency
- Elementary/Comprehension
Courses/Certifications Sections
Courses:
{
"courses": [
{
"title": "Course Title",
"institution": "Platform or Institution",
"courseLogo": "platform-logo.png", // Optional, in static/images/courses/
"location": "Online or City",
"date": "2024-03", // YYYY-MM or range
"duration": "40 hours",
"shortDescription": "Brief course overview",
"responsibilities": [ // Optional: detailed course content
"Topic 1 covered",
"Topic 2 covered"
]
}
]
}
Certifications:
{
"certifications": [
{
"name": "Certification Name",
"issuer": "Issuing Organization",
"date": "2024-01",
"description": "What this certification covers"
}
]
}
Course Logos:
- Place in
static/images/courses/ - Examples: Codecademy, LinkedIn Learning, Coursera logos
Awards Section
{
"awards": [
{
"title": "Award Title",
"issuer": "Issuing Organization",
"date": "09 2023", // MM YYYY format
"shortDescription": "Brief description of award",
"responsibilities": [ // Optional: what you did to earn it
"Achievement 1",
"Achievement 2"
],
"awardLogo": "award-logo.png" // Optional, in static/images/companies/
}
]
}
References Section
{
"references": [
{
"title": "Full reference text",
"url": "https://example.com",
"type": "recommendation", // recommendation, portfolio, profile, cv, presentation
"textBefore": "Text before link", // Optional
"linkText": "Clickable text", // Optional: bold linked text
"textAfter": "text after link" // Optional
}
]
}
Types:
recommendation: Reference lettersportfolio: Online portfolioprofile: LinkedIn, GitHub, etc.cv: Other CV versionspresentation: Presentation letters
Example rendering:
Text before link Clickable text text after link
Other Section
{
"other": {
"driverLicense": "<strong>Type B</strong>" // HTML allowed
}
}
Add any miscellaneous information here.
Meta Section
{
"meta": {
"version": "2025-11-09", // Your CV version
"lastUpdated": "2025-11-08", // Last update date
"format": "JSON Resume Extended",
"language": "en" // "en" or "es"
}
}
Update lastUpdated when you make changes.
Adding/Removing Sections
Removing Sections
Option 1: Empty the array/object
{
"awards": [],
"courses": []
}
Templates automatically hide empty sections.
Option 2: Remove from template
Edit templates/cv-content.html and delete the section:
<!-- Remove this entire block -->
{{if .CV.Awards}}
<section id="awards" class="cv-section">
...
</section>
{{end}}
Adding New Sections
Step 1: Add to JSON
{
"volunteering": [
{
"organization": "Organization Name",
"role": "Volunteer Role",
"startDate": "2020-01",
"endDate": "present",
"description": "What you did"
}
]
}
Step 2: Add to Go model (internal/models/cv.go)
type CV struct {
// ... existing fields ...
Volunteering []Volunteering `json:"volunteering"`
}
type Volunteering struct {
Organization string `json:"organization"`
Role string `json:"role"`
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
Description string `json:"description"`
}
Step 3: Add to template (templates/cv-content.html)
<!-- Add wherever you want it to appear -->
{{if .CV.Volunteering}}
<section id="volunteering" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:hand-heart" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Voluntariado{{else}}Volunteering{{end}}
</h3>
</summary>
{{range .CV.Volunteering}}
<div class="volunteering-item">
<strong>{{.Role}} - {{.Organization}}</strong><br>
<small>{{.StartDate}} / {{.EndDate}}</small>
<p>{{.Description}}</p>
</div>
{{end}}
</details>
</section>
{{end}}
Step 4: Rebuild and test
make build
make dev
Visual Customization
Colors & Fonts
Location: static/css/main.css
Color Scheme
CSS Variables (lines 6-15):
:root {
--bg-gray: rgb(82, 86, 89); /* Page background */
--sidebar-gray: #d1d4d2; /* Sidebar background */
--black-bar: #2b2b2b; /* Top action bar */
--paper-white: #ffffff; /* CV paper background */
--text-dark: rgb(0, 0, 0); /* Main text */
--text-gray: rgb(51, 51, 51); /* Secondary text */
--accent-blue: #0066cc; /* Links and accents */
--border-gray: #dddddd; /* Borders */
}
Changing Colors:
/* Example: Blue theme */
:root {
--bg-gray: #1a365d; /* Dark blue background */
--sidebar-gray: #e3f2fd; /* Light blue sidebar */
--accent-blue: #2196f3; /* Bright blue links */
--black-bar: #0d47a1; /* Deep blue bar */
}
/* Example: Dark theme */
:root {
--bg-gray: #1e1e1e; /* Dark background */
--sidebar-gray: #2d2d2d; /* Dark sidebar */
--paper-white: #252526; /* Dark paper */
--text-dark: #d4d4d4; /* Light text */
--text-gray: #9d9d9d; /* Gray text */
--accent-blue: #569cd6; /* Blue accent */
}
Fonts
Current fonts (line 4):
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&family=Source+Sans+Pro:wght@400;600&family=Inter:wght@400;500;600;700&display=swap');
Changing fonts:
- Choose fonts from Google Fonts
- Update import:
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Open+Sans:wght@400;600&display=swap');
- Update font family (line 24):
body {
font-family: 'Roboto', 'Open Sans', -apple-system, system-ui, sans-serif;
}
- Customize headings (add after body):
h1, h2, h3 {
font-family: 'Playfair Display', serif;
}
Font sizes:
/* Increase base font size */
body {
font-size: 18px; /* Default: 16px */
}
/* Adjust specific elements */
.cv-name {
font-size: 2.5rem; /* Larger name */
}
.section-title {
font-size: 1.3rem; /* Larger section titles */
}
Layout Changes
Page Width
Current: A4 paper size (210mm x 297mm)
Wider layout:
.cv-page {
max-width: 250mm; /* Wider (default: fit to A4) */
min-height: 330mm; /* Taller */
}
US Letter:
.cv-page {
max-width: 8.5in; /* Letter width */
min-height: 11in; /* Letter height */
}
Grid Adjustments
Sidebar widths (find .page-content section):
.page-content {
display: grid;
grid-template-columns: 200px 1fr; /* Left sidebar | Main */
}
/* Page 2: Main | Right sidebar */
.page-2 .page-content {
grid-template-columns: 1fr 200px; /* Make sidebar wider */
}
Adjust gap:
.page-content {
gap: 25px; /* Space between sidebar and main (default: 20px) */
}
Responsive Breakpoints
Mobile view (add to end of CSS):
@media screen and (max-width: 768px) {
.page-content {
grid-template-columns: 1fr; /* Stack vertically */
}
.cv-sidebar {
order: 2; /* Sidebars after main content */
}
.cv-main {
order: 1; /* Main content first */
}
}
Print Styles
Current print styles handle page breaks. Customize:
@media print {
body {
background: white; /* No background texture when printing */
}
.cv-page {
box-shadow: none; /* Remove shadow for printing */
margin: 0;
page-break-after: always;
}
/* Hide elements when printing */
.action-bar {
display: none;
}
/* Prevent page breaks inside elements */
.experience-item,
.project-item {
page-break-inside: avoid;
}
}
Branding
Adding Your Logo
Step 1: Prepare logo
- Format: PNG with transparency preferred
- Size: 200x80px (width x height)
- Location:
static/images/logo.png
Step 2: Add to template (templates/cv-content.html)
Add after header:
<div class="cv-header">
<div class="cv-logo">
<img src="/static/images/logo.png" alt="Your Brand Logo">
</div>
<!-- Existing header content -->
</div>
Step 3: Style logo (static/css/main.css):
.cv-logo {
text-align: center;
margin-bottom: 20px;
}
.cv-logo img {
max-width: 200px;
height: auto;
}
Favicon Replacement
Current favicon: Browser tab icon
Replace:
- Create favicon (16x16, 32x32, 48x48 px)
- Use Favicon Generator
- Place in
static/images/favicon/ - Update in
templates/index.html:
<head>
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon.png">
</head>
Custom Icons
Current: Using Iconify
Change icons in templates:
<!-- Old -->
<iconify-icon icon="mdi:office-building" width="24" height="24"></iconify-icon>
<!-- New (browse icons at iconify.design) -->
<iconify-icon icon="fa6-solid:building" width="24" height="24"></iconify-icon>
<iconify-icon icon="carbon:user-avatar" width="24" height="24"></iconify-icon>
Use custom icons:
Replace with <img> tags:
<img src="/static/images/icons/custom-icon.svg" alt="Icon" class="section-icon">
Template Customization
Templates use Go template syntax. Basic knowledge helps but isn't required for simple changes.
Go Template Syntax Basics
Variables:
{{.CV.Personal.Name}} <!-- Output variable -->
{{.CV.Personal.Email}}
Conditionals:
{{if .CV.Awards}}
<!-- Only show if awards exist -->
<section>Awards here</section>
{{end}}
{{if eq .Lang "es"}}
Español
{{else}}
English
{{end}}
Loops:
{{range .CV.Experience}}
<div>{{.Position}} at {{.Company}}</div>
{{end}}
Safe HTML (allow HTML in content):
{{.Description | safeHTML}}
Modifying cv-content.html
Location: templates/cv-content.html
Example: Change Section Order
Move "Education" before "Experience":
- Find Education section (around line 50)
- Cut the entire
<section id="education">...</section>block - Paste it before the Experience section (line 86)
Example: Change Header Layout
Current (lines 35-46):
<div class="cv-header">
<div class="cv-header-content">
<div class="cv-header-left">
<h1 class="cv-name">Moreno Rubio, Juan Andrés</h1>
<p class="years-experience">{{.YearsOfExperience}} years</p>
<div class="intro-text">{{.CV.Summary}}</div>
</div>
<div class="cv-photo">
<img src="/static/images/profile/dni.jpeg" alt="{{.CV.Personal.Name}}">
</div>
</div>
</div>
Customize: Change name format
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
<!-- Instead of hardcoded "Moreno Rubio, Juan Andrés" -->
Add contact info to header:
<div class="cv-header-left">
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
<p class="cv-contact">
{{.CV.Personal.Email}} | {{.CV.Personal.Phone}}
</p>
<div class="intro-text">{{.CV.Summary}}</div>
</div>
Example: Custom Section
Add "Hobbies" section:
<section id="hobbies" class="cv-section">
<h3 class="section-title">
<iconify-icon icon="mdi:heart" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Aficiones{{else}}Hobbies{{end}}
</h3>
<ul>
{{range .CV.Hobbies}}
<li>{{.}}</li>
{{end}}
</ul>
</section>
Don't forget to add to JSON and Go model!
Adding Custom Template Functions
Location: main.go (where templates are loaded)
Example: Add function to format dates
// In main.go, find template loading section and add:
funcMap := template.FuncMap{
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
},
"formatDate": func(dateStr string) string {
// Parse YYYY-MM format and return "Month Year"
parts := strings.Split(dateStr, "-")
if len(parts) != 2 {
return dateStr
}
months := []string{"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"}
monthNum, _ := strconv.Atoi(parts[1])
return months[monthNum] + " " + parts[0]
},
}
tmpl := template.New("").Funcs(funcMap)
Use in template:
<small>{{formatDate .StartDate}} / {{formatDate .EndDate}}</small>
Conditional Rendering
Show section only in long version:
<div class="long-only">
<!-- Only visible when user toggles to long CV -->
{{range .Responsibilities}}
<li>{{. | safeHTML}}</li>
{{end}}
</div>
Show section only in short version:
<div class="short-desc">
{{.ShortDescription | safeHTML}}
</div>
Language-specific content:
{{if eq .Lang "es"}}
<p>Contenido en español</p>
{{else}}
<p>Content in English</p>
{{end}}
Analytics Configuration
This template includes a self-hosted analytics implementation as a learning example. You have three options:
Option 1: Configure Your Own Analytics
If you want to use self-hosted analytics:
-
Set up your analytics server (Matomo, Plausible, or similar)
-
Update tracking code in
templates/index.html:// Find this section near the end of the file var _paq = window._paq = window._paq || []; _paq.push(['trackPageView']); _paq.push(['enableLinkTracking']); var u="https://YOUR-ANALYTICS-SERVER.COM/"; // Replace this _paq.push(['setTrackerUrl', u+'matomo.php']); _paq.push(['setSiteId', 'YOUR-SITE-ID']); // Replace this -
Update CSP headers in
internal/middleware/security.go:// Find the CSP policy and update these directives: "script-src 'self' 'unsafe-inline' https://YOUR-ANALYTICS-SERVER.COM; " + "connect-src 'self' https://YOUR-ANALYTICS-SERVER.COM; " -
Update PRIVACY.md with your own privacy policy details
Option 2: Remove Analytics Entirely
If you don't want analytics:
-
Remove tracking code from
templates/index.html:- Delete the entire
<!-- Matomo Analytics -->section (usually at the end of<body>)
- Delete the entire
-
Simplify CSP headers in
internal/middleware/security.go:// Remove analytics domains from: "script-src 'self' 'unsafe-inline'; " + // Remove analytics domain "connect-src 'self'; " // Remove analytics domain -
Update PRIVACY.md:
- Simplify to state no tracking is used
- Or remove the file if not needed
Option 3: Use Alternative Analytics Service
If you prefer Google Analytics, Plausible Cloud, or another service:
- Replace tracking code in
templates/index.htmlwith your provider's script - Update CSP headers with your provider's domains
- Update PRIVACY.md according to your provider's data handling
- Note: External services may require additional privacy disclosures (GDPR, CCPA)
Testing Analytics
After configuration:
# Start development server
make dev
# Visit site in browser
open http://localhost:1999
# Check browser console for errors
# Verify analytics requests in Network tab
Security Note: Always use HTTPS in production to protect analytics data in transit.
Privacy Compliance
Important legal considerations:
- ✅ Add cookie consent banner if required in your jurisdiction
- ✅ Create privacy policy explaining data collection
- ✅ Provide opt-out mechanism
- ✅ Comply with GDPR, CCPA, or local privacy laws
- ✅ Update privacy policy when changing analytics providers
See 10-PRIVACY.md for privacy policy template.
Advanced Customization
Adding New Languages (Beyond en/es)
Step 1: Create JSON file
cp data/cv-en.json data/cv-fr.json
# Edit cv-fr.json with French content
Step 2: Update Go model validation (internal/models/cv.go)
func LoadCV(lang string) (*CV, error) {
// Validate language
if lang != "en" && lang != "es" && lang != "fr" {
return nil, fmt.Errorf("unsupported language: %s", lang)
}
// ... rest of function
}
Step 3: Add language selector to template
<div class="language-selector">
<a href="/?lang=en">English</a> |
<a href="/?lang=es">Español</a> |
<a href="/?lang=fr">Français</a>
</div>
Step 4: Update all {{if eq .Lang "es"}} conditions:
{{if eq .Lang "es"}}
Español
{{else if eq .Lang "fr"}}
Français
{{else}}
English
{{end}}
PDF Export Customization
The CV site includes a powerful PDF export feature with multiple customization options. Users can download their CV as a PDF with different configurations.
How PDF Export Works
When a user clicks "Download PDF", the server:
- Takes a snapshot of the CV webpage using headless Chrome
- Applies special print-optimized CSS styles (
@media print) - Generates a professional PDF document
- Downloads it with a descriptive filename
PDF Export Parameters
The PDF export endpoint accepts 4 parameters that let users customize their PDF:
1. Language (lang)
en- English versiones- Spanish version- Default:
en - Example:
/export/pdf?lang=es
2. Length (length)
short- Concise version with summaries onlylong- Detailed version with full descriptions and bullet points- Default:
short - Example:
/export/pdf?length=long
3. Icons (icons)
show- Display all company logos, project icons, and visual elementshide- Text-only version without icons (more formal)- Default:
show - Example:
/export/pdf?icons=hide
4. Version (version)
extended- Full styling with colors and visual elementsclean- Minimal, print-optimized design- Default:
extended - Example:
/export/pdf?version=clean
Example URLs
# Short, clean English CV (most compact)
http://localhost:1999/export/pdf?lang=en&length=short&version=clean
# Long, detailed Spanish CV with all icons
http://localhost:1999/export/pdf?lang=es&length=long&icons=show&version=long
# Use defaults (English, short, with icons, extended)
http://localhost:1999/export/pdf
Filename Convention
PDFs are automatically named following this pattern:
CV-{Your-Name}-{lang}-{length}-{version}.pdf
Examples:
CV-Juan-Andrés-Moreno-Rubio-en-short-clean.pdfCV-Jane-Smith-es-long-extended.pdf
Customizing PDF Appearance
PDF appearance is controlled by @media print CSS rules. The print styles are already optimized for A4 paper, but you can customize them:
Location: static/css/08-contexts/_print.css (or your main CSS file)
Common customizations:
@media print {
/* Change paper size */
@page {
size: Letter portrait; /* US Letter instead of A4 */
margin: 10mm; /* Adjust margins */
}
/* Customize colors */
.section-title {
color: #000000 !important; /* Force black text */
}
/* Hide specific elements */
.cv-footer {
display: none !important;
}
/* Adjust font sizes */
.cv-name {
font-size: 18pt !important;
}
}
Frontend Integration
Add a download button to your templates:
<!-- Simple link -->
<a href="/export/pdf?lang=en&length=short&version=clean" download>
Download PDF
</a>
<!-- HTMX button (recommended) -->
<button
hx-get="/export/pdf?lang=en&length=short&version=clean"
hx-trigger="click">
📥 Download Clean PDF
</button>
<!-- Dynamic button with user's current language -->
<button
hx-get="/export/pdf?lang={{.Lang}}&length=short&version=clean"
hx-trigger="click">
Download PDF
</button>
Tips for Best PDF Results
- Keep it concise: Use
length=shortfor 1-2 page CVs - Professional look: Use
version=clean&icons=hidefor formal applications - Colorful: Use
version=long&icons=showfor creative industries - Test before sharing: Always preview the PDF before sending to employers
- File size: Short versions ~1.5-2MB, Long versions ~2-2.5MB
Troubleshooting PDF Export
Problem: PDF generation times out
- Solution: Reduce content in your JSON files, or increase timeout in
internal/pdf/generator.go
Problem: Fonts look wrong in PDF
- Solution: Ensure fonts are loaded correctly in your HTML head section
Problem: Images don't appear in PDF
- Solution: Check that image paths are absolute or relative to server root
Problem: Colors are washed out
- Solution: Add
-webkit-print-color-adjust: exact !important;to your print CSS
Advanced: Customizing PDF Generation
If you need more control over PDF generation, you can modify:
File: internal/handlers/cv.go (ExportPDF function)
- Add new parameters
- Change cookie mappings
- Modify filename pattern
File: internal/pdf/generator.go
- Adjust PDF settings (margins, paper size)
- Change timeout duration
- Modify chromedp options
See 3-API.md for complete API documentation.
Additional Export Formats
Adding Word Export
Step 1: Install library
go get github.com/nguyenthenguyen/docx
Step 2: Create export handler (new file internal/export/docx.go)
package export
import (
"github.com/nguyenthenguyen/docx"
"your-cv/internal/models"
)
func GenerateDOCX(cv *models.CV, filename string) error {
doc := docx.NewFile()
// Add content
doc.AddHeading(cv.Personal.Name, 1)
doc.AddParagraph(cv.Summary)
// Add experience
for _, exp := range cv.Experience {
doc.AddHeading(exp.Position + " - " + exp.Company, 2)
for _, resp := range exp.Responsibilities {
doc.AddListItem(resp)
}
}
return doc.Save(filename)
}
Step 3: Add route in main.go
http.HandleFunc("/download/docx", func(w http.ResponseWriter, r *http.Request) {
lang := r.URL.Query().Get("lang")
cv, _ := models.LoadCV(lang)
filename := "/tmp/cv.docx"
export.GenerateDOCX(cv, filename)
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
w.Header().Set("Content-Disposition", "attachment; filename=cv.docx")
http.ServeFile(w, r, filename)
})
API Endpoints
Add JSON API for CV data:
In main.go:
// JSON API endpoint
http.HandleFunc("/api/cv", func(w http.ResponseWriter, r *http.Request) {
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = "en"
}
cv, err := models.LoadCV(lang)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS
json.NewEncoder(w).Encode(cv)
})
Usage:
curl http://localhost:1999/api/cv?lang=en | jq .
Database Integration (Replacing JSON)
For dynamic CVs that update frequently:
Step 1: Choose database (PostgreSQL example)
go get github.com/lib/pq
Step 2: Create database schema
CREATE TABLE cv_data (
id SERIAL PRIMARY KEY,
language VARCHAR(2) NOT NULL,
content JSONB NOT NULL,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_language ON cv_data(language);
Step 3: Update LoadCV function
func LoadCVFromDB(lang string, db *sql.DB) (*CV, error) {
var content []byte
err := db.QueryRow(
"SELECT content FROM cv_data WHERE language = $1 ORDER BY updated_at DESC LIMIT 1",
lang,
).Scan(&content)
if err != nil {
return nil, err
}
var cv CV
if err := json.Unmarshal(content, &cv); err != nil {
return nil, err
}
return &cv, nil
}
Step 4: Add admin panel to update CV through web interface
Testing Your Changes
Local Testing Workflow
1. Make changes
# Edit JSON
nano data/cv-en.json
# Edit CSS
nano static/css/main.css
# Edit templates
nano templates/cv-content.html
2. Test immediately (with hot-reload):
# Start development server
make dev
# Or manually
GO_ENV=development TEMPLATE_HOT_RELOAD=true go run main.go
3. Open browser
http://localhost:1999
http://localhost:1999?lang=es
4. Check for errors
# Watch terminal for errors
# Check browser console (F12)
Validating JSON
Online validators:
Command-line validation:
# Using Python
python3 -m json.tool data/cv-en.json
# Using jq
jq . data/cv-en.json
# Should output formatted JSON without errors
Common JSON errors:
- Missing comma:
"name": "John" "title": "Dev"← Need comma - Trailing comma:
["item1", "item2",]← Remove last comma - Unescaped quotes:
"He said "hello""← Use"He said \"hello\"" - Wrong brackets:
{...]← Mismatched brackets
Browser Testing Checklist
Visual checks:
- Profile photo displays correctly
- Company logos appear (or fallback icons)
- All sections render
- No overlapping text
- Colors look correct
- Links are clickable and work
- Icons display (Iconify loaded)
Functionality checks:
- Language toggle works (
?lang=envs?lang=es) - Print view looks good (Cmd/Ctrl+P)
- PDF download works
- Long/short CV toggle works (if implemented)
- Responsive on mobile (if implemented)
Testing commands:
# Test health endpoint
curl http://localhost:1999/health
# Test English version
curl http://localhost:1999/?lang=en | head -100
# Test Spanish version
curl http://localhost:1999/?lang=es | head -100
# Test PDF generation
curl http://localhost:1999/download/pdf?lang=en --output test.pdf
open test.pdf
PDF Export Verification
Check PDF quality:
# Generate PDF
curl http://localhost:1999/download/pdf?lang=en --output cv.pdf
# Open and verify
open cv.pdf # macOS
xdg-open cv.pdf # Linux
start cv.pdf # Windows
# Check file size
ls -lh cv.pdf
# Should be ~500KB - 2MB depending on images
PDF checklist:
- All content visible (not cut off)
- Page breaks in correct places
- Images/logos render correctly
- Links are clickable (if viewing digitally)
- Text is selectable (not rasterized)
- Colors accurate
- No weird formatting issues
Debug PDF issues:
# If PDF generation fails, check:
# 1. Chromium installed
chromium-browser --version
# 2. Chromium path
which chromium-browser
# 3. Set environment variable if needed
export CHROME_BIN=/usr/bin/chromium-browser
Examples
Example 1: Complete Personal Rebrand
Goal: Replace all content with your own
Steps:
# 1. Backup original
cp data/cv-en.json data/cv-en.json.backup
# 2. Edit personal info
nano data/cv-en.json
# Update: personal, summary, experience, education, skills, projects
# 3. Replace photo
cp ~/my-headshot.jpg static/images/profile/dni.jpeg
# 4. Update colors to match your brand
nano static/css/main.css
# Change: --accent-blue to your brand color
# 5. Test
make dev
open http://localhost:1999
# 6. Generate PDF
curl http://localhost:1999/download/pdf?lang=en --output my-cv.pdf
Example 2: Academic CV Style
Goal: Convert to academic CV format
Changes needed:
- Reorder sections: Education first, then Publications, then Experience
- Add Publications section (follow pattern from "Adding New Sections")
- Remove "Projects" and "Awards" sections
- Change styling to more conservative colors
CSS changes:
:root {
--accent-blue: #013c77; /* Conservative dark blue */
--bg-gray: #f4f4f4; /* Light background */
}
body {
font-family: 'Times New Roman', serif; /* Traditional font */
}
Example 3: Portfolio Website Integration
Goal: Use CV data to populate portfolio website
Create new template templates/portfolio.html:
<!DOCTYPE html>
<html>
<head>
<title>{{.CV.Personal.Name}} - Portfolio</title>
<link rel="stylesheet" href="/static/css/portfolio.css">
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/projects">Projects</a>
<a href="/cv">CV</a>
</nav>
<header>
<h1>{{.CV.Personal.Name}}</h1>
<p>{{.CV.Personal.Title}}</p>
</header>
<section class="projects">
{{range .CV.Projects}}
<article>
<h2>{{.Title}}</h2>
<p>{{.ShortDescription}}</p>
<a href="{{.URL}}">View Project</a>
</article>
{{end}}
</section>
</body>
</html>
Add route in main.go:
http.HandleFunc("/portfolio", func(w http.ResponseWriter, r *http.Request) {
// Render portfolio template using same CV data
})
Example 4: Multi-Language Support (Adding French)
Complete implementation:
1. Create French JSON:
cp data/cv-en.json data/cv-fr.json
# Translate all content to French
2. Update model validation (internal/models/cv.go):
func LoadCV(lang string) (*CV, error) {
if lang != "en" && lang != "es" && lang != "fr" {
return nil, fmt.Errorf("unsupported language: %s", lang)
}
// ...
}
3. Update template conditionals (all instances):
{{if eq .Lang "fr"}}
Français text
{{else if eq .Lang "es"}}
Texto en español
{{else}}
English text
{{end}}
4. Add language selector:
<div class="lang-selector">
<a href="/?lang=en" {{if eq .Lang "en"}}class="active"{{end}}>EN</a>
<a href="/?lang=es" {{if eq .Lang "es"}}class="active"{{end}}>ES</a>
<a href="/?lang=fr" {{if eq .Lang "fr"}}class="active"{{end}}>FR</a>
</div>
Common Customization Patterns
Pattern 1: Responsive Sidebar
Make sidebars collapse on mobile:
@media screen and (max-width: 768px) {
.page-content {
grid-template-columns: 1fr;
}
.cv-sidebar {
display: none; /* Hide sidebars on mobile */
}
/* Or show as accordion */
.cv-sidebar details {
margin-bottom: 15px;
}
}
Pattern 2: Dark Mode Toggle
Add dark mode switch:
CSS:
/* Dark mode variables */
[data-theme="dark"] {
--bg-gray: #1e1e1e;
--sidebar-gray: #2d2d2d;
--paper-white: #252526;
--text-dark: #d4d4d4;
--text-gray: #9d9d9d;
}
JavaScript (add to template):
<script>
function toggleDarkMode() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
// Load saved preference
const saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
}
</script>
Pattern 3: Skills Progress Bars
Add visual skill levels:
Template (in skills section):
{{range $category.Items}}
<div class="skill-item">
<span class="skill-name">{{.}}</span>
<div class="skill-bar">
<div class="skill-level" style="width: {{$category.Proficiency}}0%;"></div>
</div>
</div>
{{end}}
CSS:
.skill-bar {
background: #e0e0e0;
height: 8px;
border-radius: 4px;
overflow: hidden;
margin-top: 4px;
}
.skill-level {
background: var(--accent-blue);
height: 100%;
transition: width 0.3s ease;
}
Troubleshooting Customization
Issue: Changes Not Appearing
Solutions:
# 1. Hard refresh browser
# Press Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux)
# 2. Clear browser cache
# Or use private/incognito window
# 3. Restart server if Go code changed
pkill cv-server
make dev
# 4. Check for errors
# Look in terminal and browser console (F12)
Issue: JSON Parse Error
Solutions:
# Validate JSON syntax
python3 -m json.tool data/cv-en.json
# Common fixes:
# - Add missing commas between items
# - Remove trailing commas in arrays/objects
# - Escape quotes in strings: \" instead of "
# - Check matching brackets: { } [ ]
Issue: Template Rendering Error
Solutions:
# Check error message in terminal
# Common issues:
# - Undefined variable: Check spelling, case-sensitivity
# - Wrong field name: Verify against models/cv.go
# - Missing | safeHTML for HTML content
Issue: Styling Not Applied
Solutions:
# 1. Verify CSS file loaded
# Check browser Network tab (F12) for main.css
# 2. Check CSS syntax
# Use browser DevTools to inspect elements
# 3. Check specificity
# Use !important to test: color: red !important;
# 4. Verify class names match
# Template: class="cv-name"
# CSS: .cv-name { ... }
Next Steps
After customization:
- Test thoroughly with checklist above
- Generate PDF and verify quality
- Deploy using 8-DEPLOYMENT.md guide
- Set up CI/CD for automatic deployments
- Share your customized CV!
Further Resources:
Need Help?
- Check existing issues on GitHub
- Open new issue with details
- Include error messages and screenshots
Happy customizing! 🎨