1675 lines
38 KiB
Markdown
1675 lines
38 KiB
Markdown
|
|
# 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](#introduction)
|
||
|
|
- [Prerequisites](#prerequisites)
|
||
|
|
- [Quick Customization](#quick-customization)
|
||
|
|
- [Content Customization](#content-customization)
|
||
|
|
- [Personal Information](#personal-information)
|
||
|
|
- [JSON Schema Explained](#json-schema-explained)
|
||
|
|
- [Adding/Removing Sections](#addingremoving-sections)
|
||
|
|
- [Visual Customization](#visual-customization)
|
||
|
|
- [Colors & Fonts](#colors--fonts)
|
||
|
|
- [Layout Changes](#layout-changes)
|
||
|
|
- [Branding](#branding)
|
||
|
|
- [Template Customization](#template-customization)
|
||
|
|
- [Advanced Customization](#advanced-customization)
|
||
|
|
- [Testing Your Changes](#testing-your-changes)
|
||
|
|
- [Examples](#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**:
|
||
|
|
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](DEPLOYMENT.md))
|
||
|
|
- **Git**: For version control (optional but recommended)
|
||
|
|
- **Browser**: For testing (Chrome/Firefox recommended)
|
||
|
|
|
||
|
|
### Optional Tools
|
||
|
|
- **JSON validator**: [JSONLint](https://jsonlint.com/)
|
||
|
|
- **Image editor**: For logo/photo preparation
|
||
|
|
- **Make**: For using Makefile commands
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Quick Customization
|
||
|
|
|
||
|
|
**Get started in 5 minutes**:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Content Customization
|
||
|
|
|
||
|
|
### Personal Information
|
||
|
|
|
||
|
|
Edit `data/cv-en.json` (and `data/cv-es.json` for Spanish):
|
||
|
|
|
||
|
|
**Location**: Top of JSON file, `personal` object
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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:
|
||
|
|
```json
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"other": {
|
||
|
|
"driverLicense": "<strong>Type B</strong>" // HTML allowed
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Add any miscellaneous information here.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
#### Meta Section
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"awards": [],
|
||
|
|
"courses": []
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Templates automatically hide empty sections.
|
||
|
|
|
||
|
|
**Option 2**: Remove from template
|
||
|
|
Edit `templates/cv-content.html` and delete the section:
|
||
|
|
```html
|
||
|
|
<!-- Remove this entire block -->
|
||
|
|
{{if .CV.Awards}}
|
||
|
|
<section id="awards" class="cv-section">
|
||
|
|
...
|
||
|
|
</section>
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Adding New Sections
|
||
|
|
|
||
|
|
**Step 1**: Add to JSON
|
||
|
|
```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`)
|
||
|
|
```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`)
|
||
|
|
```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
|
||
|
|
```bash
|
||
|
|
make build
|
||
|
|
make dev
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Visual Customization
|
||
|
|
|
||
|
|
### Colors & Fonts
|
||
|
|
|
||
|
|
**Location**: `static/css/main.css`
|
||
|
|
|
||
|
|
#### Color Scheme
|
||
|
|
|
||
|
|
**CSS Variables** (lines 6-15):
|
||
|
|
```css
|
||
|
|
: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**:
|
||
|
|
```css
|
||
|
|
/* 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):
|
||
|
|
```css
|
||
|
|
@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](https://fonts.google.com/)
|
||
|
|
2. **Update import**:
|
||
|
|
```css
|
||
|
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Open+Sans:wght@400;600&display=swap');
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Update font family** (line 24):
|
||
|
|
```css
|
||
|
|
body {
|
||
|
|
font-family: 'Roboto', 'Open Sans', -apple-system, system-ui, sans-serif;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **Customize headings** (add after body):
|
||
|
|
```css
|
||
|
|
h1, h2, h3 {
|
||
|
|
font-family: 'Playfair Display', serif;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Font sizes**:
|
||
|
|
```css
|
||
|
|
/* 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**:
|
||
|
|
```css
|
||
|
|
.cv-page {
|
||
|
|
max-width: 250mm; /* Wider (default: fit to A4) */
|
||
|
|
min-height: 330mm; /* Taller */
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**US Letter**:
|
||
|
|
```css
|
||
|
|
.cv-page {
|
||
|
|
max-width: 8.5in; /* Letter width */
|
||
|
|
min-height: 11in; /* Letter height */
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Grid Adjustments
|
||
|
|
|
||
|
|
**Sidebar widths** (find `.page-content` section):
|
||
|
|
```css
|
||
|
|
.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**:
|
||
|
|
```css
|
||
|
|
.page-content {
|
||
|
|
gap: 25px; /* Space between sidebar and main (default: 20px) */
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Responsive Breakpoints
|
||
|
|
|
||
|
|
**Mobile view** (add to end of CSS):
|
||
|
|
```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:
|
||
|
|
```css
|
||
|
|
@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:
|
||
|
|
```html
|
||
|
|
<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`):
|
||
|
|
```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](https://realfavicongenerator.net/)
|
||
|
|
3. Place in `static/images/favicon/`
|
||
|
|
4. Update in `templates/index.html`:
|
||
|
|
|
||
|
|
```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](https://iconify.design/)
|
||
|
|
|
||
|
|
**Change icons** in templates:
|
||
|
|
```html
|
||
|
|
<!-- 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:
|
||
|
|
```html
|
||
|
|
<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**:
|
||
|
|
```html
|
||
|
|
{{.CV.Personal.Name}} <!-- Output variable -->
|
||
|
|
{{.CV.Personal.Email}}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Conditionals**:
|
||
|
|
```html
|
||
|
|
{{if .CV.Awards}}
|
||
|
|
<!-- Only show if awards exist -->
|
||
|
|
<section>Awards here</section>
|
||
|
|
{{end}}
|
||
|
|
|
||
|
|
{{if eq .Lang "es"}}
|
||
|
|
Español
|
||
|
|
{{else}}
|
||
|
|
English
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Loops**:
|
||
|
|
```html
|
||
|
|
{{range .CV.Experience}}
|
||
|
|
<div>{{.Position}} at {{.Company}}</div>
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Safe HTML** (allow HTML in content):
|
||
|
|
```html
|
||
|
|
{{.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):
|
||
|
|
```html
|
||
|
|
<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
|
||
|
|
```html
|
||
|
|
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
|
||
|
|
<!-- Instead of hardcoded "Moreno Rubio, Juan Andrés" -->
|
||
|
|
```
|
||
|
|
|
||
|
|
**Add contact info to header**:
|
||
|
|
```html
|
||
|
|
<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:
|
||
|
|
|
||
|
|
```html
|
||
|
|
<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
|
||
|
|
```go
|
||
|
|
// 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**:
|
||
|
|
```html
|
||
|
|
<small>{{formatDate .StartDate}} / {{formatDate .EndDate}}</small>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Conditional Rendering
|
||
|
|
|
||
|
|
**Show section only in long version**:
|
||
|
|
```html
|
||
|
|
<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**:
|
||
|
|
```html
|
||
|
|
<div class="short-desc">
|
||
|
|
{{.ShortDescription | safeHTML}}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Language-specific content**:
|
||
|
|
```html
|
||
|
|
{{if eq .Lang "es"}}
|
||
|
|
<p>Contenido en español</p>
|
||
|
|
{{else}}
|
||
|
|
<p>Content in English</p>
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Advanced Customization
|
||
|
|
|
||
|
|
### Adding New Languages (Beyond en/es)
|
||
|
|
|
||
|
|
**Step 1**: Create JSON file
|
||
|
|
```bash
|
||
|
|
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`)
|
||
|
|
```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
|
||
|
|
```html
|
||
|
|
<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:
|
||
|
|
```html
|
||
|
|
{{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`**:
|
||
|
|
```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
|
||
|
|
```bash
|
||
|
|
go get github.com/nguyenthenguyen/docx
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2**: Create export handler (new file `internal/export/docx.go`)
|
||
|
|
```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`
|
||
|
|
```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`**:
|
||
|
|
```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**:
|
||
|
|
```bash
|
||
|
|
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)
|
||
|
|
```bash
|
||
|
|
go get github.com/lib/pq
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2**: Create database schema
|
||
|
|
```sql
|
||
|
|
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
|
||
|
|
```go
|
||
|
|
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**
|
||
|
|
```bash
|
||
|
|
# 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):
|
||
|
|
```bash
|
||
|
|
# 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**
|
||
|
|
```bash
|
||
|
|
# Watch terminal for errors
|
||
|
|
# Check browser console (F12)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Validating JSON
|
||
|
|
|
||
|
|
**Online validators**:
|
||
|
|
- [JSONLint](https://jsonlint.com/)
|
||
|
|
- [JSON Formatter](https://jsonformatter.org/)
|
||
|
|
|
||
|
|
**Command-line validation**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```css
|
||
|
|
: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`:
|
||
|
|
```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`**:
|
||
|
|
```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**:
|
||
|
|
```bash
|
||
|
|
cp data/cv-en.json data/cv-fr.json
|
||
|
|
# Translate all content to French
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Update model validation** (`internal/models/cv.go`):
|
||
|
|
```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):
|
||
|
|
```html
|
||
|
|
{{if eq .Lang "fr"}}
|
||
|
|
Français text
|
||
|
|
{{else if eq .Lang "es"}}
|
||
|
|
Texto en español
|
||
|
|
{{else}}
|
||
|
|
English text
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
**4. Add language selector**:
|
||
|
|
```html
|
||
|
|
<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:
|
||
|
|
|
||
|
|
```css
|
||
|
|
@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**:
|
||
|
|
```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):
|
||
|
|
```html
|
||
|
|
<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):
|
||
|
|
```html
|
||
|
|
{{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**:
|
||
|
|
```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**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```bash
|
||
|
|
# 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**:
|
||
|
|
```bash
|
||
|
|
# 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](DEPLOYMENT.md) guide
|
||
|
|
4. **Set up CI/CD** for automatic deployments
|
||
|
|
5. **Share** your customized CV!
|
||
|
|
|
||
|
|
**Further Resources**:
|
||
|
|
- [Go Templates Documentation](https://pkg.go.dev/text/template)
|
||
|
|
- [CSS Grid Guide](https://css-tricks.com/snippets/css/complete-guide-grid/)
|
||
|
|
- [Iconify Icon Sets](https://icon-sets.iconify.design/)
|
||
|
|
- [Google Fonts](https://fonts.google.com/)
|
||
|
|
|
||
|
|
**Need Help?**
|
||
|
|
- Check existing issues on GitHub
|
||
|
|
- Open new issue with details
|
||
|
|
- Include error messages and screenshots
|
||
|
|
|
||
|
|
Happy customizing! 🎨
|