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

---

## Quick Reference

**Quick access to common operations and endpoints.**

### Base URL
```
http://localhost:1999
```

### All Endpoints

| Endpoint | Method | Description | Common Use |
|----------|--------|-------------|------------|
| `/?lang={en\|es}` | GET | Full HTML page with CV content | Initial page load |
| `/cv?lang={en\|es}` | GET | HTML partial for HTMX swaps | Language switching |
| `/export/pdf?lang={en\|es}` | GET | Download PDF resume | Export functionality |
| `/health` | GET | Health check (JSON) | Monitoring |
| `/static/{path}` | GET | Static files (CSS, JS, images) | Assets |

### Quick curl Examples

```bash
# Health check
curl http://localhost:1999/health | jq

# English CV (full page)
curl "http://localhost:1999/?lang=en"

# Spanish CV (full page)
curl "http://localhost:1999/?lang=es"

# CV content partial (for HTMX)
curl "http://localhost:1999/cv?lang=en"

# Export PDF
curl -O -J "http://localhost:1999/export/pdf?lang=en"

# Static file with headers
curl -I http://localhost:1999/static/css/main.css
```

### HTMX Integration Pattern

```html
<!-- Language switcher button -->
<button
  hx-get="/cv?lang=en"
  hx-target="#cv-content"
  hx-swap="innerHTML"
  hx-push-url="/?lang=en">
  🇬🇧 English
</button>

<button
  hx-get="/cv?lang=es"
  hx-target="#cv-content"
  hx-swap="innerHTML"
  hx-push-url="/?lang=es">
  🇪🇸 Español
</button>

<!-- Content container -->
<main id="cv-content">
  <!-- CV content will be swapped here -->
</main>
```

### Common Error Codes

| Code | Meaning | Common Cause |
|------|---------|--------------|
| 200 | Success | Request processed correctly |
| 400 | Bad Request | Invalid `lang` parameter (not `en` or `es`) |
| 403 | Forbidden | Origin check failed (PDF endpoint) |
| 404 | Not Found | Invalid route or static file not found |
| 429 | Too Many Requests | Rate limit exceeded (PDF endpoint) |
| 500 | Server Error | Template error, data loading error, PDF generation failed |

### Performance Targets

| Endpoint | Target Response Time |
|----------|---------------------|
| `/health` | <1ms |
| `/` and `/cv` | 7-8ms |
| `/static/*` | <5ms |
| `/export/pdf` | ~3 seconds |

### Environment Configuration

```bash
# Development
PORT=1999
HOST=localhost
GO_ENV=development

# Production
PORT=1999
HOST=0.0.0.0
GO_ENV=production
ALLOWED_ORIGINS=yourdomain.com,www.yourdomain.com
```

### Need More Details?

For comprehensive documentation of each endpoint, request/response formats, and advanced usage, see the [Detailed Endpoint Documentation](#detailed-endpoint-documentation) below.

---

## Architecture

### Route Organization

All routes and middleware configuration are centralized in `internal/routes/routes.go` for clean separation of concerns:

```go
// internal/routes/routes.go
func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler {
    mux := http.NewServeMux()

    // Public routes
    mux.HandleFunc("/", cvHandler.Home)
    mux.HandleFunc("/cv", cvHandler.CVContent)
    mux.HandleFunc("/health", healthHandler.Check)

    // Protected PDF endpoint (rate limited + origin checked)
    // Static files with cache control
    // Middleware chain (Recovery → Logger → SecurityHeaders)

    return handler
}
```

This architecture provides:
- ✅ Single source of truth for routes
- ✅ Clear middleware chain visibility
- ✅ Easy route management and testing
- ✅ Clean separation from main.go

### Data Loading

**Simplified Architecture** (cache removed as of v1.1.0):
- JSON files loaded directly from disk on each request
- No caching layer (over-engineering for static CV data)
- Go's built-in file system caching is sufficient
- Data only changes on deployment/restart

## Endpoints Overview

| Method | Path | Description | HTMX Support | Protection |
|--------|------|-------------|--------------|------------|
| GET | `/` | Full CV page (home) | ❌ No | None |
| GET | `/cv` | CV content partial | ✅ Yes | None |
| GET | `/export/pdf` | PDF export | ❌ No | ✅ Rate Limited + Origin Check |
| GET | `/health` | Health check | ❌ No | None |
| GET | `/static/*` | Static files (CSS, JS, images) | ❌ No | Cache Control |

---

## 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:**
```bash
curl http://localhost:1999/
```

**curl - Spanish CV:**
```bash
curl http://localhost:1999/?lang=es
```

**Browser:**
```
http://localhost:1999/?lang=en
```

#### Error Responses

**400 Bad Request** - Invalid language parameter:
```http
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
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:**
```bash
curl -H "HX-Request: true" \
     "http://localhost:1999/cv?lang=en"
```

**curl - Spanish content:**
```bash
curl "http://localhost:1999/cv?lang=es"
```

**HTMX Integration - Language Switcher:**
```html
<!-- 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:**
```javascript
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
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
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:**
```http
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:**
```bash
curl -O -J "http://localhost:1999/export/pdf?lang=en"
# Downloads: CV-Juan-Andres-Moreno-Rubio-en.pdf
```

**curl - Download Spanish PDF:**
```bash
curl -o cv-es.pdf "http://localhost:1999/export/pdf?lang=es"
```

**wget:**
```bash
wget --content-disposition "http://localhost:1999/export/pdf?lang=en"
```

**HTML Link:**
```html
<a href="/export/pdf?lang=en" download>
  Download CV (PDF)
</a>
```

**HTMX Button (triggers download):**
```html
<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
HTTP/1.1 400 Bad Request
Content-Type: text/plain

Unsupported language. Use 'en' or 'es'
```

**500 Internal Server Error** - PDF generation failed:
```http
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:**
```json
{
  "status": "ok",
  "timestamp": "2025-11-12T17:49:00.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 |

**Note:** Cache statistics have been removed as of v1.1.0 (caching layer eliminated for simplicity).

#### Examples

**curl:**
```bash
curl http://localhost:1999/health
```

**curl with pretty print:**
```bash
curl -s http://localhost:1999/health | jq
```

**Response:**
```json
{
  "status": "ok",
  "timestamp": "2025-11-09T14:32:45.123456Z",
  "version": "1.0.0"
}
```

**Health Check Script (bash):**
```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:**
```bash
watch -n 5 'curl -s http://localhost:1999/health | jq'
```

**Load Balancer Configuration (nginx):**
```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:

```bash
# 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:**
```http
Cache-Control: public, max-age=3600
```
(1 hour)

**Production Mode:**
```http
Cache-Control: public, max-age=86400
```
(1 day)

#### Examples

**curl - Fetch CSS:**
```bash
curl http://localhost:1999/static/css/main.css
```

**curl - Check cache headers:**
```bash
curl -I http://localhost:1999/static/css/main.css
```

**Response:**
```http
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:**
```html
<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
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:

```go
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:**
```html
<!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:**
```html
<div class='error'>Unsupported language. Use 'en' or 'es'</div>
```

**HTMX Error Handling:**
```html
<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:

```html
<!-- 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:**
```bash
curl -H "Accept: application/json" \
     "http://localhost:1999/?lang=invalid"
```

**Response:**
```json
{
  "error": "Bad Request",
  "message": "Unsupported language. Use 'en' or 'es'",
  "code": 400
}
```

#### 2. HTMX Errors (HX-Request: true)

**Request:**
```bash
curl -H "HX-Request: true" \
     "http://localhost:1999/?lang=invalid"
```

**Response:**
```html
<div class='error'>Unsupported language. Use 'en' or 'es'</div>
```

#### 3. Standard HTTP Errors (default)

**Request:**
```bash
curl "http://localhost:1999/?lang=invalid"
```

**Response:**
```http
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)

```go
// 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:**
```javascript
// 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:**
```html
<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:**
```http
Cache-Control: public, max-age=3600
```
- 1 hour cache
- Allows rapid development without stale cache issues

**Production:**
```http
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:**
```go
HotReload: true  // Templates reloaded on every request
```

**Production Mode:**
```go
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:**
```nginx
gzip on;
gzip_types text/html text/css application/javascript application/json;
gzip_min_length 1000;
```

#### 2. Add ETag Support

```go
// Add to static file handler
w.Header().Set("ETag", `"`+fileHash+`"`)
```

#### 3. Add Conditional Requests

```go
if match := r.Header.Get("If-None-Match"); match == etag {
    w.WriteHeader(http.StatusNotModified)
    return
}
```

#### 4. Implement HTTP/2

```go
// Use TLS for HTTP/2 support
server.ListenAndServeTLS("cert.pem", "key.pem")
```

#### 5. PDF Generation Optimization

```go
// Cache generated PDFs for 5 minutes
pdfCache := cache.New(5*time.Minute, 10*time.Minute)
```

### Resource Limits

**Timeouts:**
```go
ReadTimeout:  15 seconds  // Request read timeout
WriteTimeout: 15 seconds  // Response write timeout
IdleTimeout:  120 seconds // Keep-alive timeout
```

**PDF Generation:**
```go
PDFTimeout: 30 seconds  // Chromedp context timeout
```

---

## Security

### Security Headers

All responses include production-grade security headers via middleware:

```http
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:**
```http
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:**
```go
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)

### Origin Checking & API Protection

**Status:** ✅ **Implemented**

The API implements origin checking to prevent external sites from hotlinking to resource-intensive endpoints like PDF generation.

**Protected Endpoints:**
- `/export/pdf` - Full protection (origin checking + rate limiting)

**Configuration via Environment Variable:**
```bash
# Development (default)
ALLOWED_ORIGINS=

# Production
ALLOWED_ORIGINS=yourdomain.com,www.yourdomain.com
```

**How It Works:**
1. Checks `Origin` header (CORS requests)
2. Falls back to `Referer` header (navigation requests)
3. Allows localhost in development
4. Blocks external domains in production
5. Requires headers in production for PDF endpoint

**Example Requests:**

```bash
# ✅ Allowed (localhost in development)
curl http://localhost:1999/export/pdf?lang=en

# ✅ Allowed (valid referer)
curl -H "Referer: http://localhost:1999/" \
  http://localhost:1999/export/pdf?lang=en

# ❌ Blocked (external referer)
curl -H "Referer: https://evil.com/" \
  http://localhost:1999/export/pdf?lang=en
# Response: 403 Forbidden
```

For more details on origin checking, see [9-SECURITY.md](9-SECURITY.md#origin-checking).

### Rate Limiting

**Status:** ✅ **Implemented**

**Current Configuration:**
- **Endpoint:** `/export/pdf`
- **Limit:** 3 requests per minute per IP
- **Window:** 1 minute (rolling)
- **Response:** 429 Too Many Requests when exceeded

**Implementation:**
```go
// Applied in main.go
pdfRateLimiter := middleware.NewRateLimiter(3, 1*time.Minute)

protectedPDFHandler := middleware.OriginChecker(
    pdfRateLimiter.Middleware(
        http.HandlerFunc(cvHandler.ExportPDF),
    ),
)
```

**Behavior:**

| Requests | Status | Response |
|----------|--------|----------|
| 1st-3rd requests | ✅ 200 OK | PDF generated |
| 4th+ request (within 1 min) | ❌ 429 Too Many Requests | Rate limit exceeded |
| After 1 minute | ✅ 200 OK | Counter reset |

**Rate Limit Response:**
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: text/plain; charset=utf-8

Rate limit exceeded. Please try again later.
```

**IP Detection:**
- Checks `X-Forwarded-For` (proxy/CDN)
- Falls back to `X-Real-IP` (alternative proxy header)
- Uses `RemoteAddr` (direct connection)
- Works with Nginx reverse proxy

**Testing Rate Limit:**
```bash
# Generate 4 PDFs quickly to test rate limiting
for i in {1..4}; do
  echo "Request $i:"
  curl -w "Status: %{http_code}\n" -o /dev/null -s \
    http://localhost:1999/export/pdf?lang=en
  sleep 1
done

# Expected output:
# Request 1: Status: 200
# Request 2: Status: 200
# Request 3: Status: 200
# Request 4: Status: 429
```

**Customizing Rate Limits:**

Edit `main.go` to adjust limits:

```go
// More restrictive: 5 per hour
pdfRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour)

// Less restrictive: 10 per minute
pdfRateLimiter := middleware.NewRateLimiter(10, 1*time.Minute)
```

For comprehensive protection documentation, see [9-SECURITY.md](9-SECURITY.md#api-protection).

### 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
- Origin checking (prevents external hotlinking)
- Rate limiting (PDF endpoint: 3 requests/min per IP)
- IP-based tracking (supports reverse proxies)

⚠️ **Recommended for Production:**
- Use HTTPS (prevents header spoofing)
- Configure `ALLOWED_ORIGINS` for your domain
- Implement request logging with IP addresses
- Add monitoring and alerting for 403/429 responses
- Consider CloudFlare for additional DDoS protection
- Set up log retention for security analysis

---

## Rate Limiting

**Current State:** ✅ **Fully Implemented** (as of v1.1.0)

### Current Implementation

Rate limiting is configured in `internal/routes/routes.go` and applied via middleware:

#### 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):**
```nginx
# 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):**
```go
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:**

```html
<!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:**

```html
<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:**

```bash
#!/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:**

```ini
# /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
```

```bash
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:**

```html
<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:**

```bash
#!/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:**
```bash
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:**
```bash
# 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:**
```bash
# macOS
brew install --cask chromium

# Ubuntu/Debian
sudo apt-get install chromium-browser

# Verify installation
which chromium
```

**Alternative:** Use Docker with Chrome pre-installed:
```dockerfile
FROM golang:1.21-alpine
RUN apk add chromium
```

---

#### Issue 3: Language Switch Not Working

**Symptom:** Clicking language buttons doesn't change content

**Diagnosis:**
```bash
# 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:**
```bash
# Check static directory
ls -la static/css/
ls -la static/js/

# Test endpoint
curl -I http://localhost:1999/static/css/main.css
```

**Solution:**
```bash
# 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:**
```bash
# 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:

```bash
# 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:**
```go
import _ "net/http/pprof"

// In main()
go func() {
  log.Println(http.ListenAndServe("localhost:6060", nil))
}()
```

**2. Profile CPU:**
```bash
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
```

**3. Profile Memory:**
```bash
go tool pprof http://localhost:6060/debug/pprof/heap
```

**4. View traces:**
```bash
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.1.0 | 2025-11-12 | Route extraction to internal/routes, cache removal, rate limiting implementation |
| 1.0.0 | 2025-11-09 | Initial release with Go rewrite |

### Related Documentation

- [README.md](../README.md) - Project overview and setup
- [CONTRIBUTING.md](../CONTRIBUTING.md) - Contribution guidelines
- [8-DEPLOYMENT.md](8-DEPLOYMENT.md) - Deployment guides

### Support

**Issues:** [GitHub Issues](https://github.com/juanatsap/cv-site/issues)
**Email:** [juan.a.moreno.rubio@gmail.com](mailto:juan.a.moreno.rubio@gmail.com)

---

**Last Updated:** November 12, 2025
**API Version:** 1.1.0
**Documentation Version:** 1.1.0
