- Removed redundant API documentation (API.md and API-QUICK-REFERENCE.md) - Added cv-app binary to gitignore to prevent committing build artifacts
38 KiB
CV Site API Documentation
Note: This is my personal CV website API documentation. While the code is open-source (MIT license), this API is designed for my personal site and may change without notice.
Overview
This API provides endpoints for a bilingual (English/Spanish) CV/resume web application built with Go's standard library. The API supports both traditional HTTP requests and HTMX-powered partial page updates for a seamless single-page application experience.
Version: 1.0.0
Base URL: http://localhost:1999 (development)
Default Port: 1999 (configurable via PORT environment variable)
Key Features
- 🌍 Bilingual Support: English and Spanish content switching
- ⚡ HTMX-Aware: Serves partial HTML for HTMX requests
- 📄 PDF Export: Generate PDF resumes using headless Chrome
- 🔒 Security Headers: Production-grade security headers (CSP, HSTS, etc.)
- 📊 Health Monitoring: JSON health check endpoint
- 🎯 No Authentication: Public CV/resume site
Content Types
- HTML:
text/html; charset=utf-8(default) - JSON:
application/json(health endpoint, errors) - PDF:
application/pdf(export endpoint)
Configuration
The server can be configured via environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
1999 |
Server port |
HOST |
localhost |
Server host |
READ_TIMEOUT |
15 |
Read timeout in seconds |
WRITE_TIMEOUT |
15 |
Write timeout in seconds |
GO_ENV |
development |
Environment mode (development/production) |
TEMPLATE_HOT_RELOAD |
true (dev) |
Enable template hot reloading |
Endpoints Overview
| Method | Path | Description | HTMX Support |
|---|---|---|---|
| GET | / |
Full CV page (home) | ❌ No |
| GET | /cv |
CV content partial | ✅ Yes |
| GET | /export/pdf |
PDF export | ❌ No |
| GET | /health |
Health check | ❌ No |
| GET | /static/* |
Static files (CSS, JS, images) | ❌ No |
Detailed Endpoint Documentation
1. GET /
Description: Renders the complete CV page with full HTML structure including header, navigation, and footer.
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
lang |
string | No | en |
Language code (en or es) |
Request Headers
No special headers required.
Response
Status Code: 200 OK
Content-Type: text/html; charset=utf-8
Response Body: Complete HTML document with:
<html>,<head>,<body>tags- Navigation and header
- Full CV content
- Footer
- Embedded styles and scripts
Examples
curl - English CV:
curl http://localhost:1999/
curl - Spanish CV:
curl http://localhost:1999/?lang=es
Browser:
http://localhost:1999/?lang=en
Error Responses
400 Bad Request - Invalid language parameter:
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Unsupported language. Use 'en' or 'es'
500 Internal Server Error - Template or data loading error:
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
Internal Server Error
Notes
- This endpoint always returns the full page structure
- Ideal for initial page load and direct navigation
- Not optimized for HTMX partial updates (use
/cvinstead) - Calculates dynamic experience duration and years of experience
- Fetches git repository dates for projects (if available)
2. GET /cv
Description: Renders only the CV content section for HTMX partial page swaps. Returns the same content as / but without the HTML wrapper.
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
lang |
string | No | en |
Language code (en or es) |
Request Headers
| Header | Value | Description |
|---|---|---|
HX-Request |
true |
Indicates HTMX request (optional, but recommended) |
Response
Status Code: 200 OK
Content-Type: text/html; charset=utf-8
Response Body: CV content HTML fragment (no <html>, <head>, or <body> tags)
Examples
curl - Test HTMX endpoint:
curl -H "HX-Request: true" \
"http://localhost:1999/cv?lang=en"
curl - Spanish content:
curl "http://localhost:1999/cv?lang=es"
HTMX Integration - Language Switcher:
<!-- English button -->
<button
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-push-url="/?lang=en">
English
</button>
<!-- Spanish button -->
<button
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-push-url="/?lang=es">
Español
</button>
JavaScript Fetch:
fetch('/cv?lang=es', {
headers: {
'HX-Request': 'true'
}
})
.then(response => response.text())
.then(html => {
document.getElementById('cv-content').innerHTML = html;
});
Error Responses
400 Bad Request - Invalid language:
HTTP/1.1 400 Bad Request
Content-Type: text/html
<div class='error'>Unsupported language. Use 'en' or 'es'</div>
500 Internal Server Error - Template error:
HTTP/1.1 500 Internal Server Error
Content-Type: text/html
<div class='error'>An error occurred. Please try again later.</div>
Notes
- HTMX-Aware: Detects
HX-Requestheader for better error formatting - Partial Content: Returns only the CV content div, not full HTML
- URL Updates: Recommended to use with
hx-push-urlfor browser history - Same Logic: Executes the same data processing as
/endpoint - Performance: Optimized for fast partial updates without full page reload
3. GET /export/pdf
Description: Generates and downloads a PDF version of the CV using headless Chrome (chromedp). The PDF is generated from the rendered HTML page.
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
lang |
string | No | en |
Language code for PDF content |
Request Headers
No special headers required.
Response
Status Code: 200 OK
Content-Type: application/pdf
Headers:
Content-Type: application/pdf
Content-Disposition: attachment; filename=CV-Juan-Andres-Moreno-Rubio-en.pdf
Content-Length: [size in bytes]
Response Body: Binary PDF data
Examples
curl - Download English PDF:
curl -O -J "http://localhost:1999/export/pdf?lang=en"
# Downloads: CV-Juan-Andres-Moreno-Rubio-en.pdf
curl - Download Spanish PDF:
curl -o cv-es.pdf "http://localhost:1999/export/pdf?lang=es"
wget:
wget --content-disposition "http://localhost:1999/export/pdf?lang=en"
HTML Link:
<a href="/export/pdf?lang=en" download>
Download CV (PDF)
</a>
HTMX Button (triggers download):
<button
hx-get="/export/pdf?lang=en"
hx-trigger="click">
📥 Download PDF
</button>
Process Flow
- Server receives PDF export request
- Constructs internal URL:
http://localhost:1999/?lang={lang} - Launches headless Chrome via chromedp
- Navigates to the CV page
- Waits for page load and rendering
- Generates PDF with print-optimized settings
- Returns PDF as downloadable file
Error Responses
400 Bad Request - Invalid language:
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Unsupported language. Use 'en' or 'es'
500 Internal Server Error - PDF generation failed:
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
Internal Server Error
Notes
- Timeout: 30-second timeout for PDF generation
- Dependencies: Requires chromedp and a Chrome/Chromium installation
- Performance: PDF generation takes 2-5 seconds typically
- Memory: Uses headless Chrome, requires adequate system memory
- Filename Pattern:
CV-Juan-Andres-Moreno-Rubio-{lang}.pdf - Internal Request: Makes internal HTTP request to
/?lang={lang} - Print Styles: Respects
@media printCSS rules
4. GET /health
Description: Health check endpoint returning server status, version, and timestamp in JSON format. Used for monitoring and load balancer health checks.
Query Parameters
None.
Request Headers
No special headers required.
Response
Status Code: 200 OK
Content-Type: application/json
Response Body:
{
"status": "ok",
"timestamp": "2025-11-09T14:32:45.123Z",
"version": "1.0.0"
}
Schema
| Field | Type | Description |
|---|---|---|
status |
string | Always "ok" if server is running |
timestamp |
string | ISO 8601 timestamp of the request |
version |
string | Application version from main.go |
Examples
curl:
curl http://localhost:1999/health
curl with pretty print:
curl -s http://localhost:1999/health | jq
Response:
{
"status": "ok",
"timestamp": "2025-11-09T14:32:45.123456Z",
"version": "1.0.0"
}
Health Check Script (bash):
#!/bin/bash
response=$(curl -s http://localhost:1999/health)
status=$(echo $response | jq -r '.status')
if [ "$status" = "ok" ]; then
echo "✅ Server is healthy"
exit 0
else
echo "❌ Server is down"
exit 1
fi
Monitoring with watch:
watch -n 5 'curl -s http://localhost:1999/health | jq'
Load Balancer Configuration (nginx):
upstream cv_backend {
server localhost:1999;
# Health check
check interval=3000 rise=2 fall=3 timeout=1000 type=http;
check_http_send "GET /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx;
}
Error Responses
Health endpoint always returns 200 OK as long as the server is running. Network errors or server down scenarios will result in connection errors:
# Server down
curl: (7) Failed to connect to localhost port 1999: Connection refused
Notes
- Always Available: No authentication or rate limiting
- Simple Check: Only confirms server is responding
- No Deep Checks: Does not validate database, templates, or external dependencies
- Monitoring: Ideal for uptime monitoring, load balancers, and Docker health checks
- Fast Response: Minimal processing, returns immediately
5. GET /static/*
Description: Serves static files including CSS, JavaScript, images, and fonts with appropriate cache headers.
Request Format
GET /static/{path/to/file}
Examples
CSS:
GET /static/css/main.css
GET /static/css/cv.css
JavaScript:
GET /static/js/htmx.min.js
GET /static/js/app.js
Images:
GET /static/images/logo.png
GET /static/images/courses/codecademy.png
Fonts:
GET /static/fonts/roboto.woff2
Response
Status Code: 200 OK (file found) or 404 Not Found
Content-Type: Determined by file extension:
.css→text/css.js→application/javascript.png→image/png.jpg→image/jpeg.woff2→font/woff2- etc.
Cache Headers:
Development Mode:
Cache-Control: public, max-age=3600
(1 hour)
Production Mode:
Cache-Control: public, max-age=86400
(1 day)
Examples
curl - Fetch CSS:
curl http://localhost:1999/static/css/main.css
curl - Check cache headers:
curl -I http://localhost:1999/static/css/main.css
Response:
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Content-Type: text/css
Content-Length: 5423
Last-Modified: Sat, 09 Nov 2024 12:00:00 GMT
/* CSS content */
HTML Integration:
<link rel="stylesheet" href="/static/css/main.css">
<script src="/static/js/htmx.min.js"></script>
<img src="/static/images/logo.png" alt="Logo">
Error Responses
404 Not Found:
HTTP/1.1 404 Not Found
Content-Type: text/plain
404 page not found
Notes
- Standard Library: Uses
http.FileServerfrom Go stdlib - Path Stripping:
/static/prefix is stripped before file lookup - Directory Listing: Disabled (shows 403 if directory accessed)
- Cache Control: Environment-aware caching strategy
- Security: Served through security middleware (CSP, X-Content-Type-Options)
- No Compression: Enable gzip compression in reverse proxy (nginx/Caddy) for production
HTMX Integration
Overview
The application is designed to work seamlessly with HTMX for dynamic, partial page updates without full page reloads. The primary use case is language switching.
HTMX Detection
The server detects HTMX requests via the HX-Request header:
isHTMX := r.Header.Get("HX-Request") != ""
Response Behavior
| Endpoint | HTMX Header | Response Type |
|---|---|---|
/ |
Any | Full HTML page |
/cv |
Present | HTML fragment |
/cv |
Absent | HTML fragment |
/export/pdf |
Any | PDF binary |
/health |
Any | JSON |
Language Switching Pattern
HTML Structure:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<nav>
<button
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-push-url="/?lang=en"
class="lang-btn">
English
</button>
<button
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-push-url="/?lang=es"
class="lang-btn">
Español
</button>
</nav>
<div id="cv-content">
<!-- CV content loaded here -->
</div>
</body>
</html>
How it works:
- User clicks language button
- HTMX intercepts click and sends GET request to
/cv?lang={lang} - HTMX adds
HX-Request: trueheader automatically - Server returns HTML fragment (CV content only)
- HTMX swaps content into
#cv-contentdiv - Browser URL updates to
/?lang={lang}(viahx-push-url) - No full page reload
HTMX Attributes Used
| Attribute | Purpose | Example |
|---|---|---|
hx-get |
Specify endpoint | hx-get="/cv?lang=en" |
hx-target |
Target element | hx-target="#cv-content" |
hx-swap |
Swap strategy | hx-swap="innerHTML" |
hx-push-url |
Update URL | hx-push-url="/?lang=en" |
hx-trigger |
Event trigger | hx-trigger="click" |
Error Handling with HTMX
When errors occur, the server checks for HX-Request header and returns appropriate HTML:
HTMX Error Response:
<div class='error'>Unsupported language. Use 'en' or 'es'</div>
HTMX Error Handling:
<div id="cv-content">
<div
hx-get="/cv?lang=en"
hx-trigger="load"
hx-swap="outerHTML">
Loading...
</div>
</div>
<script>
// Handle HTMX errors
document.body.addEventListener('htmx:responseError', function(evt) {
console.error('HTMX Error:', evt.detail.xhr.status);
alert('Failed to load content. Please try again.');
});
</script>
Out-of-Band Swaps
Currently not implemented, but could be used for updating multiple page sections:
<!-- Future enhancement -->
<div id="cv-content" hx-swap-oob="true">
<!-- New CV content -->
</div>
<div id="page-title" hx-swap-oob="true">
<h1>CV - Spanish</h1>
</div>
Error Handling
Error Response Format
The API uses context-aware error responses based on the Accept header and HX-Request header.
1. JSON Errors (Accept: application/json)
Request:
curl -H "Accept: application/json" \
"http://localhost:1999/?lang=invalid"
Response:
{
"error": "Bad Request",
"message": "Unsupported language. Use 'en' or 'es'",
"code": 400
}
2. HTMX Errors (HX-Request: true)
Request:
curl -H "HX-Request: true" \
"http://localhost:1999/?lang=invalid"
Response:
<div class='error'>Unsupported language. Use 'en' or 'es'</div>
3. Standard HTTP Errors (default)
Request:
curl "http://localhost:1999/?lang=invalid"
Response:
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Unsupported language. Use 'en' or 'es'
Common Error Codes
| Status Code | Error Type | When It Occurs |
|---|---|---|
| 400 | Bad Request | Invalid language parameter |
| 404 | Not Found | Static file not found, invalid route |
| 500 | Internal Server Error | Template rendering error, data loading error, PDF generation error |
Error Types (Internal)
// Defined in internal/handlers/errors.go
BadRequestError() // 400 - Client error
NotFoundError() // 404 - Resource not found
InternalError() // 500 - Server error (details hidden)
TemplateError() // 500 - Template rendering failed
DataLoadError() // 500 - Failed to load CV data
Internal vs Public Errors
Internal Errors (500):
- Details logged server-side only
- Generic message returned to client:
"Internal Server Error" - Includes: template errors, data loading errors, PDF generation errors
Public Errors (4xx):
- Descriptive message returned to client
- Details safe to expose
- Includes: invalid language, not found
Error Logging
All errors are logged server-side with context:
ERROR [GET /]: Error rendering template: index.html
CLIENT ERROR [GET /]: Unsupported language. Use 'en' or 'es' (status: 400)
Error Handling Best Practices
Client-Side:
// Fetch with error handling
async function loadCV(lang) {
try {
const response = await fetch(`/cv?lang=${lang}`);
if (!response.ok) {
const error = await response.json();
console.error('API Error:', error.message);
return;
}
const html = await response.text();
document.getElementById('cv-content').innerHTML = html;
} catch (err) {
console.error('Network Error:', err);
alert('Failed to connect to server');
}
}
HTMX Error Handling:
<script>
// Global HTMX error handler
document.body.addEventListener('htmx:responseError', function(evt) {
const status = evt.detail.xhr.status;
if (status === 400) {
alert('Invalid request. Please try again.');
} else if (status >= 500) {
alert('Server error. Please try again later.');
}
});
// Network error handler
document.body.addEventListener('htmx:sendError', function(evt) {
alert('Cannot connect to server. Check your connection.');
});
</script>
Performance & Caching
Cache Headers Strategy
Static Files (CSS, JS, Images)
Development:
Cache-Control: public, max-age=3600
- 1 hour cache
- Allows rapid development without stale cache issues
Production:
Cache-Control: public, max-age=86400
- 1 day cache
- Reduces server load and bandwidth
- Improves page load performance
Dynamic Content (HTML)
No cache headers set (browser default behavior):
- CV content changes rarely
- Language switching requires fresh content
- Could add
Cache-Control: private, max-age=300for 5-minute cache
PDF Export
No cache headers:
- Generated on-demand
- Content-Disposition triggers download
- Browser doesn't cache downloads by default
Template Caching
Development Mode:
HotReload: true // Templates reloaded on every request
Production Mode:
HotReload: false // Templates compiled once on startup
Performance Metrics
| Endpoint | Avg Response Time | Notes |
|---|---|---|
/ |
5-15ms | Template rendering + data loading |
/cv |
5-15ms | Same as / (uses same logic) |
/export/pdf |
2-5 seconds | Headless Chrome rendering |
/health |
<1ms | Simple JSON response |
/static/* |
<5ms | Direct file serving |
Optimization Recommendations
1. Add Response Compression
Use reverse proxy (nginx/Caddy) for gzip compression:
nginx:
gzip on;
gzip_types text/html text/css application/javascript application/json;
gzip_min_length 1000;
2. Add ETag Support
// Add to static file handler
w.Header().Set("ETag", `"`+fileHash+`"`)
3. Add Conditional Requests
if match := r.Header.Get("If-None-Match"); match == etag {
w.WriteHeader(http.StatusNotModified)
return
}
4. Implement HTTP/2
// Use TLS for HTTP/2 support
server.ListenAndServeTLS("cert.pem", "key.pem")
5. PDF Generation Optimization
// Cache generated PDFs for 5 minutes
pdfCache := cache.New(5*time.Minute, 10*time.Minute)
Resource Limits
Timeouts:
ReadTimeout: 15 seconds // Request read timeout
WriteTimeout: 15 seconds // Response write timeout
IdleTimeout: 120 seconds // Keep-alive timeout
PDF Generation:
PDFTimeout: 30 seconds // Chromedp context timeout
Security
Security Headers
All responses include production-grade security headers via middleware:
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()...
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'...
Production Only:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content Security Policy (CSP)
default-src 'self';
script-src 'self' 'unsafe-inline' https://unpkg.com https://code.iconify.design;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.iconify.design;
frame-ancestors 'self';
base-uri 'self';
form-action 'self'
External Resources Allowed:
- HTMX from unpkg.com
- Icons from iconify.design
- Fonts from Google Fonts
CORS
Not explicitly configured (same-origin only):
- No
Access-Control-Allow-Originheaders - API not designed for cross-origin access
- Could be added if needed for external integrations
Authentication
Current State: None
This is a public CV/resume site with no authentication or authorization.
Future Considerations:
- Add admin authentication for CV data updates
- Implement API key for PDF export rate limiting
- Add OAuth for private CV access
Input Validation
Language Parameter:
if lang != "en" && lang != "es" {
return BadRequestError("Unsupported language. Use 'en' or 'es'")
}
Path Traversal Protection:
- Static file handler uses
http.FileServer(built-in protection) - No user-controlled file paths
- No database queries (no SQL injection risk)
Rate Limiting
Current State: Not implemented
Recommendations:
1. Nginx Rate Limiting:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /export/pdf {
limit_req zone=api burst=5;
}
2. Go Middleware:
// Example rate limiter
import "golang.org/x/time/rate"
func RateLimit(next http.Handler) http.Handler {
limiter := rate.NewLimiter(10, 20) // 10 req/s, burst 20
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", 429)
return
}
next.ServeHTTP(w, r)
})
}
Security Best Practices
✅ Implemented:
- Security headers (CSP, X-Frame-Options, etc.)
- HSTS in production
- Input validation
- Error message sanitization (internal errors hidden)
- Timeouts on all operations
- Graceful shutdown
⚠️ Recommended:
- Add rate limiting (especially for PDF export)
- Implement request logging with IP addresses
- Add monitoring and alerting
- Use HTTPS in production
- Implement fail2ban for abuse prevention
Rate Limiting
Current State: Not implemented
Recommended Implementation
Per-Endpoint Limits
| Endpoint | Recommended Limit | Burst |
|---|---|---|
/ |
20 req/min | 10 |
/cv |
30 req/min | 15 |
/export/pdf |
5 req/min | 2 |
/health |
Unlimited | - |
/static/* |
100 req/min | 50 |
Implementation Options
1. Nginx (Recommended for Production):
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=general:10m rate=20r/m;
limit_req_zone $binary_remote_addr zone=pdf:10m rate=5r/m;
server {
location / {
limit_req zone=general burst=10 nodelay;
}
location /cv {
limit_req zone=general burst=15 nodelay;
}
location /export/pdf {
limit_req zone=pdf burst=2 nodelay;
limit_req_status 429;
}
}
2. Go Middleware (for standalone deployment):
import (
"golang.org/x/time/rate"
"sync"
)
type IPRateLimiter struct {
ips map[string]*rate.Limiter
mu *sync.RWMutex
r rate.Limit
b int
}
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
return &IPRateLimiter{
ips: make(map[string]*rate.Limiter),
mu: &sync.RWMutex{},
r: r,
b: b,
}
}
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()
limiter, exists := i.ips[ip]
if !exists {
limiter = rate.NewLimiter(i.r, i.b)
i.ips[ip] = limiter
}
return limiter
}
func RateLimitMiddleware(limiter *IPRateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
limiter := limiter.GetLimiter(ip)
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
3. Cloudflare Rate Limiting (easiest):
- Configure in Cloudflare dashboard
- No code changes required
- Enterprise-grade DDoS protection
Examples & Use Cases
Use Case 1: Language Switching with HTMX
Scenario: User wants to switch between English and Spanish CV without page reload.
Implementation:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My CV</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
.lang-btn { margin: 5px; padding: 10px 20px; }
.lang-btn.active { background: #007bff; color: white; }
</style>
</head>
<body>
<header>
<nav>
<button
class="lang-btn active"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-push-url="/?lang=en"
onclick="setActive(this)">
English
</button>
<button
class="lang-btn"
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-push-url="/?lang=es"
onclick="setActive(this)">
Español
</button>
</nav>
</header>
<main id="cv-content">
<!-- Initial content loaded here -->
<?php include 'cv-content-en.html'; ?>
</main>
<script>
function setActive(btn) {
document.querySelectorAll('.lang-btn').forEach(b =>
b.classList.remove('active')
);
btn.classList.add('active');
}
</script>
</body>
</html>
Use Case 2: PDF Export with Progress Indicator
Scenario: User clicks "Download PDF" and sees progress while PDF generates.
Implementation:
<button id="pdf-btn" onclick="downloadPDF('en')">
📥 Download PDF (English)
</button>
<div id="pdf-status" style="display:none;">
<div class="spinner"></div>
<p>Generating PDF...</p>
</div>
<script>
async function downloadPDF(lang) {
const btn = document.getElementById('pdf-btn');
const status = document.getElementById('pdf-status');
// Show progress
btn.disabled = true;
status.style.display = 'block';
try {
const response = await fetch(`/export/pdf?lang=${lang}`);
if (!response.ok) {
throw new Error('PDF generation failed');
}
// Create blob and download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `CV-${lang}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
status.textContent = '✅ PDF downloaded!';
setTimeout(() => {
status.style.display = 'none';
}, 2000);
} catch (error) {
console.error('PDF Error:', error);
status.textContent = '❌ Failed to generate PDF';
} finally {
btn.disabled = false;
}
}
</script>
<style>
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
Use Case 3: Health Monitoring Script
Scenario: DevOps team wants continuous health monitoring with alerts.
Implementation:
#!/bin/bash
# health-monitor.sh
API_URL="http://localhost:1999/health"
ALERT_EMAIL="admin@example.com"
CHECK_INTERVAL=30 # seconds
check_health() {
response=$(curl -s -w "\n%{http_code}" "$API_URL")
http_code=$(echo "$response" | tail -n 1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -eq 200 ]; then
status=$(echo "$body" | jq -r '.status')
version=$(echo "$body" | jq -r '.version')
timestamp=$(echo "$body" | jq -r '.timestamp')
echo "[$(date)] ✅ Healthy - Status: $status, Version: $version"
return 0
else
echo "[$(date)] ❌ Unhealthy - HTTP $http_code"
send_alert "CV Server is down (HTTP $http_code)"
return 1
fi
}
send_alert() {
message=$1
echo "$message" | mail -s "CV Server Alert" "$ALERT_EMAIL"
# Optional: Slack webhook
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-Type: application/json' \
-d "{\"text\": \"🚨 $message\"}"
}
# Main loop
while true; do
check_health
sleep $CHECK_INTERVAL
done
Run as systemd service:
# /etc/systemd/system/cv-health-monitor.service
[Unit]
Description=CV Server Health Monitor
After=network.target
[Service]
Type=simple
User=monitor
ExecStart=/usr/local/bin/health-monitor.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable cv-health-monitor
sudo systemctl start cv-health-monitor
Use Case 4: Integration with Analytics
Scenario: Track CV views and PDF downloads with analytics.
Implementation:
<script>
// Google Analytics 4 integration
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
// Track page views
document.body.addEventListener('htmx:afterSwap', function(evt) {
const lang = new URLSearchParams(window.location.search).get('lang');
gtag('event', 'cv_view', {
'language': lang || 'en',
'event_category': 'engagement',
'event_label': 'CV View'
});
});
// Track PDF downloads
async function trackPDFDownload(lang) {
gtag('event', 'pdf_download', {
'language': lang,
'event_category': 'conversion',
'event_label': 'PDF Download',
'value': 1
});
// Then trigger actual download
window.location.href = `/export/pdf?lang=${lang}`;
}
</script>
<button onclick="trackPDFDownload('en')">
Download PDF
</button>
Use Case 5: Curl Testing Suite
Complete API testing with curl:
#!/bin/bash
# test-api.sh - Complete API test suite
API_URL="http://localhost:1999"
PASSED=0
FAILED=0
test_endpoint() {
name=$1
url=$2
expected_status=$3
echo "Testing: $name"
status=$(curl -s -o /dev/null -w "%{http_code}" "$url")
if [ "$status" -eq "$expected_status" ]; then
echo " ✅ PASS (HTTP $status)"
((PASSED++))
else
echo " ❌ FAIL (Expected $expected_status, got $status)"
((FAILED++))
fi
}
echo "=== CV API Test Suite ==="
echo
# Test 1: Health check
test_endpoint "Health Check" "$API_URL/health" 200
# Test 2: Home page (English)
test_endpoint "Home - English" "$API_URL/?lang=en" 200
# Test 3: Home page (Spanish)
test_endpoint "Home - Spanish" "$API_URL/?lang=es" 200
# Test 4: Invalid language
test_endpoint "Invalid Language" "$API_URL/?lang=fr" 400
# Test 5: CV content endpoint
test_endpoint "CV Content" "$API_URL/cv?lang=en" 200
# Test 6: Static CSS
test_endpoint "Static CSS" "$API_URL/static/css/main.css" 200
# Test 7: PDF export (takes time)
echo "Testing: PDF Export (this may take a few seconds)"
pdf_status=$(curl -s -o /tmp/test-cv.pdf -w "%{http_code}" "$API_URL/export/pdf?lang=en")
if [ "$pdf_status" -eq 200 ] && [ -s /tmp/test-cv.pdf ]; then
pdf_size=$(wc -c < /tmp/test-cv.pdf)
echo " ✅ PASS (HTTP $pdf_status, PDF size: $pdf_size bytes)"
((PASSED++))
rm /tmp/test-cv.pdf
else
echo " ❌ FAIL (HTTP $pdf_status or empty file)"
((FAILED++))
fi
# Test 8: 404 Not Found
test_endpoint "404 Not Found" "$API_URL/nonexistent" 404
# Test 9: Health response format
echo "Testing: Health Response Format"
health=$(curl -s "$API_URL/health")
if echo "$health" | jq -e '.status == "ok"' > /dev/null 2>&1; then
echo " ✅ PASS (Valid JSON with status:ok)"
((PASSED++))
else
echo " ❌ FAIL (Invalid JSON or missing status)"
((FAILED++))
fi
# Summary
echo
echo "=== Test Summary ==="
echo "Passed: $PASSED"
echo "Failed: $FAILED"
echo "Total: $((PASSED + FAILED))"
if [ $FAILED -eq 0 ]; then
echo "✅ All tests passed!"
exit 0
else
echo "❌ Some tests failed"
exit 1
fi
Run tests:
chmod +x test-api.sh
./test-api.sh
Troubleshooting
Common Issues
Issue 1: Server Won't Start
Symptom:
❌ Failed to initialize templates: open templates/index.html: no such file or directory
Solution:
# Ensure you're in the project root
cd /path/to/cv-site
# Check template directory exists
ls -la templates/
# Check file permissions
chmod 644 templates/*.html
Issue 2: PDF Generation Fails
Symptom:
PDF generation failed: chrome not found
Solution:
# macOS
brew install --cask chromium
# Ubuntu/Debian
sudo apt-get install chromium-browser
# Verify installation
which chromium
Alternative: Use Docker with Chrome pre-installed:
FROM golang:1.21-alpine
RUN apk add chromium
Issue 3: Language Switch Not Working
Symptom: Clicking language buttons doesn't change content
Diagnosis:
# Check HTMX is loaded
curl -I http://localhost:1999/static/js/htmx.min.js
# Test endpoint directly
curl "http://localhost:1999/cv?lang=es"
Solution:
- Verify HTMX script tag in HTML
- Check browser console for JavaScript errors
- Ensure
hx-targetmatches actual element ID
Issue 4: Static Files Not Loading
Symptom: CSS/JS 404 errors
Diagnosis:
# Check static directory
ls -la static/css/
ls -la static/js/
# Test endpoint
curl -I http://localhost:1999/static/css/main.css
Solution:
# Create missing directories
mkdir -p static/css static/js static/images
# Check file paths in HTML match actual paths
grep -r 'href="/static' templates/
Issue 5: Slow PDF Generation
Symptom: PDF export takes >10 seconds
Diagnosis:
# Time the request
time curl -o test.pdf "http://localhost:1999/export/pdf?lang=en"
Solutions:
- Optimize CSS: Remove unused styles
- Reduce images: Compress or lazy-load images
- Cache PDFs: Implement 5-minute cache
- Increase timeout: Adjust chromedp timeout
- Use worker pool: Queue PDF generation requests
Debug Mode
Enable verbose logging:
# Set log flags
export GO_ENV=development
export LOG_LEVEL=debug
# Run server
go run main.go
Logs will show:
[GET] /cv?lang=es 127.0.0.1:54321 - 200 (12.5ms)
[GET] /export/pdf?lang=en 127.0.0.1:54322 - 200 (3.2s)
ERROR [GET /]: Error loading CV data
Performance Profiling
1. Enable pprof:
import _ "net/http/pprof"
// In main()
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
2. Profile CPU:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
3. Profile Memory:
go tool pprof http://localhost:6060/debug/pprof/heap
4. View traces:
curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out
go tool trace trace.out
Appendix
Environment Variables Reference
| Variable | Type | Default | Description |
|---|---|---|---|
PORT |
int | 1999 |
Server listen port |
HOST |
string | localhost |
Server bind address |
GO_ENV |
string | development |
Environment mode |
READ_TIMEOUT |
int | 15 |
Request read timeout (seconds) |
WRITE_TIMEOUT |
int | 15 |
Response write timeout (seconds) |
TEMPLATE_DIR |
string | templates |
Template directory path |
PARTIALS_DIR |
string | templates/partials |
Partial templates path |
TEMPLATE_HOT_RELOAD |
bool | true (dev) |
Enable template hot reload |
DATA_DIR |
string | data |
CV data directory |
Response Time Targets
| Endpoint | Target | Acceptable | Action Required |
|---|---|---|---|
/ |
<50ms | <200ms | >200ms investigate |
/cv |
<50ms | <200ms | >200ms investigate |
/health |
<10ms | <50ms | >50ms investigate |
/static/* |
<20ms | <100ms | >100ms add CDN |
/export/pdf |
<3s | <5s | >5s optimize |
HTTP Status Code Reference
| Code | Name | When Used |
|---|---|---|
| 200 | OK | Successful request |
| 304 | Not Modified | Cached resource (ETag match) |
| 400 | Bad Request | Invalid language parameter |
| 404 | Not Found | Route or static file not found |
| 429 | Too Many Requests | Rate limit exceeded (if implemented) |
| 500 | Internal Server Error | Template, data, or PDF error |
| 503 | Service Unavailable | Server shutting down |
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2025-11-09 | Initial release with Go rewrite |
Related Documentation
- README.md - Project overview and setup
- CHANGELOG.md - Version history
- CONTRIBUTING.md - Contribution guidelines
Support
Issues: GitHub Issues Email: juan.a.moreno.rubio@gmail.com
Last Updated: November 9, 2025 API Version: 1.0.0 Documentation Version: 1.0.0