Files
cv-site/doc/CUSTOMIZATION.md
T
juanatsap 0e52d625a1 refactor: remove API documentation files and add binary to gitignore
- Removed redundant API documentation (API.md and API-QUICK-REFERENCE.md)
- Added cv-app binary to gitignore to prevent committing build artifacts
2025-11-09 20:48:24 +00:00

42 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

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:

  1. Basic: Edit JSON files only (name, experience, skills)
  2. Intermediate: Modify CSS styles and add images
  3. 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 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:

  1. Replace personal section in data/cv-en.json
  2. Replace summary section
  3. Replace experience section with your jobs
  4. Replace education section
  5. Update skills section
  6. Replace profile photo
  7. Update Matomo 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 tagline
  • location: Current location
  • email: Contact email (clickable in CV)
  • phone: Phone number with country code
  • dateOfBirth: Birth date (YYYY-MM-DD format)
  • placeOfBirth: Birthplace
  • citizenship: Nationality/citizenship
  • linkedin, github, domestika, website: Social/professional links
  • photo: 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 title
  • company: Company name
  • companyURL: Company website (optional, makes company name clickable)
  • companyLogo: Logo filename (place in static/images/companies/)
  • location: Office location
  • startDate: Start date (YYYY-MM format)
  • endDate: End date or "present" for current job
  • current: Boolean, true if still employed
  • expired: Boolean, true if 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 used
  • highlights: 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:

  1. Place logo in static/images/companies/
  2. Reference filename in companyLogo field
  3. Recommended size: 100x100px, PNG with transparency
  4. 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 field for 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:

  1. Group by category (e.g., "Programming Languages", "Databases")
  2. Order by importance (most important categories first)
  3. Use specific names (e.g., "PostgreSQL" not just "SQL")
  4. 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 gitRepoUrl to 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 letters
  • portfolio: Online portfolio
  • profile: LinkedIn, GitHub, etc.
  • cv: Other CV versions
  • presentation: 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:

  1. Choose fonts from Google Fonts
  2. Update import:
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Open+Sans:wght@400;600&display=swap');
  1. Update font family (line 24):
body {
    font-family: 'Roboto', 'Open Sans', -apple-system, system-ui, sans-serif;
}
  1. 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

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:

  1. Create favicon (16x16, 32x32, 48x48 px)
  2. Use Favicon Generator
  3. Place in static/images/favicon/
  4. 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":

  1. Find Education section (around line 50)
  2. Cut the entire <section id="education">...</section> block
  3. 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

CRITICAL: If you use this template, you MUST update or remove the Matomo analytics configuration.

Option 1: Use Your Own Matomo Instance

Step 1: Set up your own Matomo server

  • Install Matomo on your server or use a hosted service
  • Create a new website in Matomo dashboard
  • Note your Site ID and server URL

Step 2: Update tracking code in templates/index.html (around line 635-649)

Find this section:

<!-- Matomo -->
<script>
    var _paq = window._paq = window._paq || [];
    _paq.push(['trackPageView']);
    _paq.push(['enableLinkTracking']);
    (function() {
        var u="https://matomo.drolo.club/";  // ← CHANGE THIS
        _paq.push(['setTrackerUrl', u+'matomo.php']);
        _paq.push(['setSiteId', '4']);      // ← CHANGE THIS
        var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
        g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
    })();
</script>
<!-- End Matomo Code -->

Change:

  1. Line 642: Replace https://matomo.drolo.club/ with your Matomo server URL
  2. Line 644: Replace '4' with your Site ID from Matomo dashboard

Step 3: Update Content Security Policy in internal/middleware/security.go (lines 33, 37)

Find and update these lines:

// Line 33: Allow your Matomo domain for scripts
"script-src 'self' 'unsafe-inline' https://unpkg.com https://code.iconify.design https://YOUR-MATOMO-DOMAIN.com; " +

// Line 37: Allow your Matomo domain for API calls
"connect-src 'self' https://api.iconify.design https://YOUR-MATOMO-DOMAIN.com; " +

Replace https://matomo.drolo.club with your Matomo domain.

Step 4: Create your own privacy policy

  • Copy PRIVACY.md and update with your contact information
  • Update cookie disclosure with your Matomo server details
  • Ensure compliance with GDPR/privacy laws in your jurisdiction

Option 2: Remove Matomo Entirely

If you don't want analytics:

Step 1: Remove tracking code from templates/index.html

Delete lines 623-649 (the entire Matomo section):

// Delete this entire block:
// Track HTMX navigation events with Matomo
document.body.addEventListener('htmx:afterSwap', function(evt) { ... });

<!-- Matomo -->
<script> ... </script>
<!-- End Matomo Code -->

Step 2: Remove Matomo from CSP headers in internal/middleware/security.go

Remove https://matomo.drolo.club from lines 33 and 37:

// Before:
"script-src 'self' 'unsafe-inline' https://unpkg.com https://code.iconify.design https://matomo.drolo.club; " +

// After:
"script-src 'self' 'unsafe-inline' https://unpkg.com https://code.iconify.design; " +

Step 3: Update or remove PRIVACY.md

  • Remove analytics section
  • Keep only essential privacy information

Option 3: Use Google Analytics or Other Service

If you prefer Google Analytics, Plausible, or another service:

  1. Remove Matomo code (see Option 2 above)
  2. Add your analytics provider's code in the same location
  3. Update CSP headers to allow your analytics domain
  4. Update PRIVACY.md with your analytics provider's details
  5. Ensure compliance with privacy regulations (GDPR, CCPA, etc.)

Testing Analytics

After configuration:

# 1. Build and run
go build -o cv-server . && ./cv-server

# 2. Open browser with developer tools
open http://localhost:1999

# 3. Check Console for errors
# - Should see Matomo requests if configured
# - Should see no errors about blocked scripts

# 4. Verify in your analytics dashboard
# - Real-time visitors should show your session
# - Language switches should track as pageviews

Privacy Compliance

Important legal considerations:

  • Add cookie banner if required in your jurisdiction (EU requires consent)
  • 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 PRIVACY.md for template privacy policy.


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}}

Custom PDF Styles

PDF generation uses same templates but renders with Chromium. Customize print styles:

Add to static/css/main.css:

@media print {
    /* PDF-specific styles */
    body {
        background: white;
    }

    .cv-page {
        box-shadow: none;
        margin: 0;
    }

    /* Custom PDF header/footer */
    @page {
        margin: 20mm;

        @top-center {
            content: "Your Name - CV";
        }

        @bottom-right {
            content: "Page " counter(page) " of " counter(pages);
        }
    }

    /* Prevent orphans/widows */
    p, li {
        orphans: 3;
        widows: 3;
    }
}

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=en vs ?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:

  1. Reorder sections: Education first, then Publications, then Experience
  2. Add Publications section (follow pattern from "Adding New Sections")
  3. Remove "Projects" and "Awards" sections
  4. Change styling to more conservative colors

CSS changes:

:root {
    --accent-blue: #2c3e50;  /* 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:

  1. Test thoroughly with checklist above
  2. Generate PDF and verify quality
  3. Deploy using DEPLOYMENT.md guide
  4. Set up CI/CD for automatic deployments
  5. 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! 🎨