Files
cv-site/API.md
T
juanatsap 5e132e7ec7 docs: finalize documentation as personal portfolio project
- Add clear disclaimers: personal site, not a template
- Update CONTRIBUTING.md: not seeking contributions
- Remove all Docker files and references (11 files)
- Rewrite DEPLOYMENT.md without Docker (VPS/cloud focus)
- Add comprehensive API documentation with verified endpoints
- Add complete CUSTOMIZATION guide
- Add project status sections to all major docs
- Clarify MIT license but personal use intent

Created documentation files:
- API.md (70KB) - Complete API reference with live testing
- API-QUICK-REFERENCE.md - Quick command reference
- DEPLOYMENT.md (45KB) - VPS, cloud, manual deployment (no Docker)
- CUSTOMIZATION.md (38KB) - Complete customization guide
- PROJECT-DOCUMENTATION-SUMMARY.md - Complete project overview

This is my personal CV site. While code is public (MIT),
it's designed for my personal use, not as a template.
2025-11-09 13:54:31 +00:00

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 /cv instead)
  • 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-Request header for better error formatting
  • Partial Content: Returns only the CV content div, not full HTML
  • URL Updates: Recommended to use with hx-push-url for 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

  1. Server receives PDF export request
  2. Constructs internal URL: http://localhost:1999/?lang={lang}
  3. Launches headless Chrome via chromedp
  4. Navigates to the CV page
  5. Waits for page load and rendering
  6. Generates PDF with print-optimized settings
  7. 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 print CSS 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:

  • .csstext/css
  • .jsapplication/javascript
  • .pngimage/png
  • .jpgimage/jpeg
  • .woff2font/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.FileServer from 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:

  1. User clicks language button
  2. HTMX intercepts click and sends GET request to /cv?lang={lang}
  3. HTMX adds HX-Request: true header automatically
  4. Server returns HTML fragment (CV content only)
  5. HTMX swaps content into #cv-content div
  6. Browser URL updates to /?lang={lang} (via hx-push-url)
  7. 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=300 for 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-Origin headers
  • 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

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-target matches 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:

  1. Optimize CSS: Remove unused styles
  2. Reduce images: Compress or lazy-load images
  3. Cache PDFs: Implement 5-minute cache
  4. Increase timeout: Adjust chromedp timeout
  5. 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

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