949c9a0351
- Move docs/ contents to doc/ with proper numbering: - CONTACT-FORM-QUICKSTART.md → 17-CONTACT-FORM.md - SECURITY-AUDIT-REPORT.md → 18-SECURITY-AUDIT.md - SECURITY.md → 19-SECURITY-IMPLEMENTATION.md - Delete duplicate/redundant files from docs/: - CMD-K-COMMAND-BAR.md (duplicate of 16-CMD-K-API.md) - CONTACT_FORM_IMPLEMENTATION.md (overlaps with quickstart) - SECURITY-IMPLEMENTATION-SUMMARY.md (summary of audit) - Update doc/README.md with new document references - Update test counts to 39 test files across all READMEs - Update all "Last Updated" dates to 2025-12-01 - Add new API endpoints documentation (text, cmd-k, contact, toggles) - Update PROJECT-MEMORY.md with new features and correct paths
2251 lines
52 KiB
Markdown
2251 lines
52 KiB
Markdown
# 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 |
|
|
| `/text?lang={en\|es}` | GET | Plain text CV for terminal/AI | curl, text browsers |
|
|
| `/api/cmd-k?lang={en\|es}` | GET | CMD+K command palette data (JSON) | ninja-keys integration |
|
|
| `/export/pdf?lang={en\|es}&length={short\|long}&icons={show\|hide}&version={extended\|clean}` | GET | Download PDF resume with parameters | 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 (short, clean version)
|
|
curl -O -J "http://localhost:1999/export/pdf?lang=en&length=short&version=clean"
|
|
|
|
# CMD+K command palette data (JSON)
|
|
curl -s http://localhost:1999/api/cmd-k | jq '.experiences | length'
|
|
|
|
# Plain text CV
|
|
curl http://localhost:1999/text?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 | `/text` | Plain text CV for CLI/terminal | ❌ No | None |
|
|
| GET | `/api/cmd-k` | CMD+K command palette data (JSON) | ❌ No | Cache Control (1h) |
|
|
| POST | `/api/contact` | Contact form submission | ✅ Yes | BrowserOnly + Rate Limit + CSRF |
|
|
| GET | `/switch-language` | Language switching | ✅ Yes | None |
|
|
| GET | `/toggle/length` | CV length toggle | ✅ Yes | None |
|
|
| GET | `/toggle/icons` | Icon visibility toggle | ✅ Yes | None |
|
|
| GET | `/toggle/theme` | Theme toggle | ✅ Yes | None |
|
|
| GET | `/export/pdf` | PDF export | ❌ No | ✅ Rate Limited + Origin Check |
|
|
| GET | `/cv-jamr-{year}-{lang}.pdf` | Shortcut PDF download routes | ❌ No | Redirect to /export/pdf |
|
|
| 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
|
|
|
|
```
|
|
|
|
**500 Internal Server Error** - Template or data loading error:
|
|
```http
|
|
HTTP/1.1 500 Internal Server Error
|
|
Content-Type: text/plain
|
|
|
|
```
|
|
|
|
#### 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
|
|
|
|
```
|
|
|
|
**500 Internal Server Error** - Template error:
|
|
```http
|
|
HTTP/1.1 500 Internal Server Error
|
|
Content-Type: text/html
|
|
|
|
```
|
|
|
|
#### 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 /text
|
|
|
|
**Description:** Returns a plain text version of the CV, optimized for CLI tools (curl, wget) and text browsers (lynx, w3m). Auto-detected via User-Agent header.
|
|
|
|
#### Query Parameters
|
|
|
|
| Parameter | Type | Required | Default | Description |
|
|
|-----------|------|----------|---------|-------------|
|
|
| `lang` | string | No | `en` | Language code (`en` or `es`) |
|
|
|
|
#### Response
|
|
|
|
**Status Code:** `200 OK`
|
|
|
|
**Content-Type:** `text/plain; charset=utf-8`
|
|
|
|
**Response Body:** 80-character wrapped plain text CV with ASCII formatting
|
|
|
|
#### Examples
|
|
|
|
```bash
|
|
# Get plain text CV (auto-detected via curl User-Agent)
|
|
curl http://localhost:1999/text
|
|
|
|
# Spanish version
|
|
curl http://localhost:1999/text?lang=es
|
|
|
|
# View in text browser
|
|
lynx http://localhost:1999/text
|
|
```
|
|
|
|
#### Notes
|
|
|
|
- Returns CV content formatted for terminal display
|
|
- 80-character line width for optimal terminal viewing
|
|
- Unicode characters properly handled
|
|
- Useful for AI assistants reading CV content
|
|
|
|
---
|
|
|
|
### 4. GET /api/cmd-k
|
|
|
|
**Description:** Returns JSON data for the CMD+K command palette (ninja-keys integration). Provides dynamic entries for experiences, projects, and courses that can be searched.
|
|
|
|
#### Query Parameters
|
|
|
|
| Parameter | Type | Required | Default | Description |
|
|
|-----------|------|----------|---------|-------------|
|
|
| `lang` | string | No | `en` | Language code (`en` or `es`) |
|
|
|
|
#### Response
|
|
|
|
**Status Code:** `200 OK`
|
|
|
|
**Content-Type:** `application/json`
|
|
|
|
**Cache-Control:** `public, max-age=3600` (1 hour)
|
|
|
|
**Response Body:**
|
|
```json
|
|
{
|
|
"experiences": [
|
|
{"id": "exp-1", "title": "Senior Developer", "section": "experience", "keywords": "..."}
|
|
],
|
|
"projects": [
|
|
{"id": "proj-1", "title": "Project Name", "section": "projects", "keywords": "..."}
|
|
],
|
|
"courses": [
|
|
{"id": "course-1", "title": "Course Name", "section": "courses", "keywords": "..."}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Examples
|
|
|
|
```bash
|
|
# Get CMD+K data
|
|
curl -s http://localhost:1999/api/cmd-k | jq
|
|
|
|
# Count experiences
|
|
curl -s http://localhost:1999/api/cmd-k | jq '.experiences | length'
|
|
```
|
|
|
|
#### Notes
|
|
|
|
- Used by ninja-keys web component for command palette
|
|
- Cached for 1 hour to reduce server load
|
|
- Entries include scroll-to-section functionality
|
|
|
|
---
|
|
|
|
### 5. POST /api/contact
|
|
|
|
**Description:** Contact form submission endpoint with comprehensive security middleware chain.
|
|
|
|
#### Request Headers
|
|
|
|
| Header | Required | Description |
|
|
|--------|----------|-------------|
|
|
| `HX-Request` | Yes | Must be `true` (browser validation) |
|
|
| `Referer` or `Origin` | Yes | Must match allowed origins |
|
|
| `Content-Type` | Yes | `application/x-www-form-urlencoded` |
|
|
|
|
#### Request Body
|
|
|
|
| Field | Type | Required | Validation |
|
|
|-------|------|----------|------------|
|
|
| `name` | string | Yes | 2-100 characters |
|
|
| `email` | string | Yes | Valid email format |
|
|
| `message` | string | Yes | 10-5000 characters |
|
|
| `_csrf` | string | Yes | Valid CSRF token from session |
|
|
|
|
#### Security Middleware
|
|
|
|
1. **BrowserOnly** - Blocks curl/Postman/bots (requires HX-Request header)
|
|
2. **Rate Limiting** - 5 submissions per hour per IP
|
|
3. **CSRF Protection** - Token validation against session
|
|
|
|
#### Response
|
|
|
|
**Status Code:** `200 OK` (success) or `400/403/429` (error)
|
|
|
|
**Content-Type:** `text/html` (HTMX partial)
|
|
|
|
#### Error Responses
|
|
|
|
| Code | Reason |
|
|
|------|--------|
|
|
| 400 | Validation failed (missing fields, invalid email) |
|
|
| 403 | Security check failed (no browser headers, invalid CSRF) |
|
|
| 429 | Rate limit exceeded (5/hour per IP) |
|
|
| 500 | Email sending failed |
|
|
|
|
#### Notes
|
|
|
|
- See `docs/CONTACT-FORM-QUICKSTART.md` for implementation details
|
|
- SMTP configuration via environment variables
|
|
- Returns HTMX partial for seamless form updates
|
|
|
|
---
|
|
|
|
### 6. GET /switch-language
|
|
|
|
**Description:** HTMX endpoint for language switching. Returns updated UI elements.
|
|
|
|
#### Query Parameters
|
|
|
|
| Parameter | Type | Required | Default | Description |
|
|
|-----------|------|----------|---------|-------------|
|
|
| `lang` | string | Yes | - | Target language (`en` or `es`) |
|
|
|
|
#### Response
|
|
|
|
Returns HTMX partial with updated language-specific content.
|
|
|
|
---
|
|
|
|
### 7. GET /toggle/{preference}
|
|
|
|
**Description:** HTMX endpoints for CV preference toggles.
|
|
|
|
#### Endpoints
|
|
|
|
- `GET /toggle/length` - Toggle CV length (short/long)
|
|
- `GET /toggle/icons` - Toggle icon visibility (show/hide)
|
|
- `GET /toggle/theme` - Toggle theme (default/clean)
|
|
|
|
#### Response
|
|
|
|
Returns HTMX partial with updated toggle state.
|
|
|
|
---
|
|
|
|
### 8. GET /cv-jamr-{year}-{lang}.pdf
|
|
|
|
**Description:** Shortcut routes for default CV PDF downloads. Redirects to `/export/pdf` with appropriate parameters.
|
|
|
|
#### Examples
|
|
|
|
```
|
|
/cv-jamr-2025-en.pdf → /export/pdf?lang=en&length=short&icons=show&version=clean
|
|
/cv-jamr-2025-es.pdf → /export/pdf?lang=es&length=short&icons=show&version=clean
|
|
```
|
|
|
|
---
|
|
|
|
### 9. 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 with customizable parameters for language, length, icons, and version.
|
|
|
|
#### Query Parameters
|
|
|
|
| Parameter | Type | Required | Default | Description |
|
|
|-----------|------|----------|---------|-------------|
|
|
| `lang` | string | No | `en` | Language code (`en` or `es`) |
|
|
| `length` | string | No | `short` | CV length (`short` for summary, `long` for detailed) |
|
|
| `icons` | string | No | `show` | Icon visibility (`show` or `hide`) |
|
|
| `version` | string | No | `extended` | CV version (`extended` for default, `clean` for minimal) |
|
|
|
|
#### 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-Andrés-Moreno-Rubio-{lang}-{length}-{version}.pdf
|
|
Content-Length: [size in bytes]
|
|
```
|
|
|
|
**Response Body:** Binary PDF data
|
|
|
|
#### Examples
|
|
|
|
**curl - Download English PDF (short, clean version):**
|
|
```bash
|
|
curl -O -J "http://localhost:1999/export/pdf?lang=en&length=short&icons=show&version=clean"
|
|
# Downloads: CV-Juan-Andrés-Moreno-Rubio-en-short-clean.pdf
|
|
```
|
|
|
|
**curl - Download Spanish PDF (long, extended version):**
|
|
```bash
|
|
curl -o cv-es.pdf "http://localhost:1999/export/pdf?lang=es&length=long&icons=hide&version=long"
|
|
# Downloads: CV-Juan-Andrés-Moreno-Rubio-es-long-extended.pdf
|
|
```
|
|
|
|
**curl - Use defaults:**
|
|
```bash
|
|
curl -O -J "http://localhost:1999/export/pdf"
|
|
# Downloads: CV-Juan-Andrés-Moreno-Rubio-en-short-extended.pdf
|
|
```
|
|
|
|
**wget:**
|
|
```bash
|
|
wget --content-disposition "http://localhost:1999/export/pdf?lang=en&length=short&version=clean"
|
|
```
|
|
|
|
**HTML Link:**
|
|
```html
|
|
<a href="/export/pdf?lang=en&length=short&icons=show&version=clean" download>
|
|
Download CV (PDF)
|
|
</a>
|
|
```
|
|
|
|
**HTMX Button (triggers download):**
|
|
```html
|
|
<button
|
|
hx-get="/export/pdf?lang=en&length=short&icons=show&version=clean"
|
|
hx-trigger="click">
|
|
📥 Download PDF
|
|
</button>
|
|
```
|
|
|
|
#### Process Flow
|
|
|
|
1. Server receives PDF export request with parameters
|
|
2. Validates all parameters (lang, length, icons, version)
|
|
3. Sets cookies for user preferences:
|
|
- `cv-language` → `{lang}`
|
|
- `cv-length` → `{length}`
|
|
- `cv-icons` → `{icons}`
|
|
- `cv-theme` → `default` or `clean` based on `{version}`
|
|
4. Constructs internal URL: `http://localhost:1999/?lang={lang}`
|
|
5. Launches headless Chrome via chromedp
|
|
6. Sets cookies in browser context
|
|
7. Navigates to the CV page
|
|
8. Waits for page load and rendering
|
|
9. Generates PDF with print-optimized settings (A4, @media print CSS)
|
|
10. Returns PDF with filename: `CV-{Name}-{lang}-{length}-{version}.pdf`
|
|
|
|
#### Error Responses
|
|
|
|
**400 Bad Request** - Invalid language:
|
|
```http
|
|
HTTP/1.1 400 Bad Request
|
|
Content-Type: text/plain
|
|
|
|
```
|
|
|
|
**400 Bad Request** - Invalid length:
|
|
```http
|
|
HTTP/1.1 400 Bad Request
|
|
Content-Type: text/plain
|
|
|
|
```
|
|
|
|
**400 Bad Request** - Invalid icons option:
|
|
```http
|
|
HTTP/1.1 400 Bad Request
|
|
Content-Type: text/plain
|
|
|
|
```
|
|
|
|
**400 Bad Request** - Invalid version:
|
|
```http
|
|
HTTP/1.1 400 Bad Request
|
|
Content-Type: text/plain
|
|
|
|
```
|
|
|
|
**500 Internal Server Error** - PDF generation failed:
|
|
```http
|
|
HTTP/1.1 500 Internal Server Error
|
|
Content-Type: text/plain
|
|
|
|
```
|
|
|
|
#### Parameter Details
|
|
|
|
**Language (`lang`):**
|
|
- `en` - English content
|
|
- `es` - Spanish content
|
|
|
|
**Length (`length`):**
|
|
- `short` - Summary version with concise descriptions
|
|
- `long` - Detailed version with full responsibilities and descriptions
|
|
|
|
**Icons (`icons`):**
|
|
- `show` - Display company logos, project icons, and section icons
|
|
- `hide` - Hide all icons for a text-only appearance
|
|
|
|
**Version (`version`):**
|
|
- `extended` - Default theme with full styling
|
|
- `clean` - Minimal theme optimized for print
|
|
|
|
#### 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-{Name}-{lang}-{length}-{version}.pdf`
|
|
- **Cookie Injection:** Parameters are injected as cookies before page rendering
|
|
- **Print Styles:** Respects `@media print` CSS rules
|
|
- **Size:** Short version ~1.5-2MB, Long version ~2-2.5MB (varies with content)
|
|
|
|
---
|
|
|
|
### 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
|
|
|
|
```
|
|
|
|
**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
|
|
|
|
```
|
|
|
|
#### 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
|
|
|
|
```
|
|
|
|
### 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
|
|
|
|
```
|
|
|
|
**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:** December 1, 2025
|
|
**API Version:** 1.2.0
|
|
**Documentation Version:** 1.2.0
|