2025-11-09 13:54:31 +00:00
# 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 |
---
2025-11-12 16:04:43 +00:00
## 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.
---
2025-11-09 13:54:31 +00:00
## Endpoints Overview
| Method | Path | Description | HTMX Support |
|--------|------|-------------|--------------|
| GET | `/` | Full CV page (home) | ❌ No |
| GET | `/cv` | CV content partial | ✅ Yes |
| GET | `/export/pdf` | PDF export | ❌ No |
| GET | `/health` | Health check | ❌ No |
| GET | `/static/*` | Static files (CSS, JS, images) | ❌ No |
---
## Detailed Endpoint Documentation
### 1. GET /
**Description: ** Renders the complete CV page with full HTML structure including header, navigation, and footer.
#### Query Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `lang` | string | No | `en` | Language code (`en` or `es` ) |
#### Request Headers
No special headers required.
#### Response
**Status Code: ** `200 OK`
**Content-Type: ** `text/html; charset=utf-8`
**Response Body: ** Complete HTML document with:
- `<html>` , `<head>` , `<body>` tags
- Navigation and header
- Full CV content
- Footer
- Embedded styles and scripts
#### Examples
**curl - English CV: **
``` 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 /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
C o n t e n t - T y p e : a p p l i c a t i o n / p d f
C o n t e n t - D i s p o s i t i o n : a t t a c h m e n t ; f i l e n a m e = C V - J u a n - A n d r e s - M o r e n o - R u b i o - e n . p d f
C o n t e n t - L e n g t h : [ s i z e i n b y t e s ]
```
**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
```
**500 Internal Server Error ** - PDF generation failed:
``` http
HTTP / 1.1 500 Internal Server Error
Content-Type : text/plain
```
#### 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-09T14:32:45.123Z" ,
"version" : "1.0.0"
}
```
#### Schema
| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Always `"ok"` if server is running |
| `timestamp` | string | ISO 8601 timestamp of the request |
| `version` | string | Application version from main.go |
#### Examples
**curl: **
``` 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
C a c h e - C o n t r o l : p u b l i c , m a x - a g e = 3 6 0 0
```
(1 hour)
**Production Mode: **
``` http
C a c h e - C o n t r o l : p u b l i c , m a x - a g e = 8 6 4 0 0
```
(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
C a c h e - C o n t r o l : p u b l i c , m a x - a g e = 3 6 0 0
```
- 1 hour cache
- Allows rapid development without stale cache issues
**Production: **
``` http
C a c h e - C o n t r o l : p u b l i c , m a x - a g e = 8 6 4 0 0
```
- 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 - F r a m e - O p t i o n s : S A M E O R I G I N
X - C o n t e n t - T y p e - O p t i o n s : n o s n i f f
X - X S S - P r o t e c t i o n : 1 ; m o d e = b l o c k
R e f e r r e r - P o l i c y : s t r i c t - o r i g i n - w h e n - c r o s s - o r i g i n
P e r m i s s i o n s - P o l i c y : g e o l o c a t i o n = ( ) , m i c r o p h o n e = ( ) , c a m e r a = ( ) , p a y m e n t = ( ) . . .
C o n t e n t - S e c u r i t y - P o l i c y : d e f a u l t - s r c ' s e l f ' ; s c r i p t - s r c ' s e l f ' ' u n s a f e - i n l i n e ' . . .
```
**Production Only: **
``` http
S t r i c t - T r a n s p o r t - S e c u r i t y : m a x - a g e = 3 1 5 3 6 0 0 0 ; i n c l u d e S u b D o m a i n s ; p r e l o a d
```
### 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)
2025-11-12 16:04:43 +00:00
**Configuration via Environment Variable: **
2025-11-09 13:54:31 +00:00
``` bash
2025-11-12 16:04:43 +00:00
# Development (default)
2025-11-09 13:54:31 +00:00
ALLOWED_ORIGINS =
2025-11-12 16:04:43 +00:00
2025-11-09 13:54:31 +00:00
# Production
2025-11-12 16:04:43 +00:00
ALLOWED_ORIGINS = yourdomain.com,www.yourdomain.com
```
2025-11-09 13:54:31 +00:00
2025-11-12 16:04:43 +00:00
**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
2025-11-09 13:54:31 +00:00
**Example Requests: **
2025-11-12 16:04:43 +00:00
``` 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 [SECURITY.md ](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 ) ,
) ,
)
2025-11-09 13:54:31 +00:00
```
2025-11-12 16:04:43 +00:00
**Behavior: **
2025-11-09 13:54:31 +00:00
2025-11-12 16:04:43 +00:00
| 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 |
2025-11-09 13:54:31 +00:00
**Rate Limit Response: **
2025-11-12 16:04:43 +00:00
``` 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 )
2025-11-09 13:54:31 +00:00
// Less restrictive: 10 per minute
pdfRateLimiter := middleware . NewRateLimiter ( 10 , 1 * time . Minute )
2025-11-12 16:04:43 +00:00
```
For comprehensive protection documentation, see [SECURITY.md ](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)
2025-11-09 13:54:31 +00:00
- 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
2025-11-12 16:04:43 +00:00
- Set up log retention for security analysis
---
2025-11-09 13:54:31 +00:00
2025-11-12 16:04:43 +00:00
## Rate Limiting
**Current State: ** Not implemented
2025-11-09 13:54:31 +00:00
2025-11-12 16:04:43 +00:00
### Recommended Implementation
#### Per-Endpoint Limits
2025-11-09 13:54:31 +00:00
| 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 : 5 px ; padding : 10 px 20 px ; }
. 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 : 4 px solid #f3f3f3 ;
border-top : 4 px solid #3498db ;
border-radius : 50 % ;
width : 40 px ;
height : 40 px ;
animation : spin 1 s linear infinite ;
margin : 20 px auto ;
}
@ keyframes spin {
0 % { transform : rotate ( 0 deg ) ; }
100 % { transform : rotate ( 360 deg ) ; }
}
< / 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.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
- [DEPLOYMENT.md ](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 )
---
2025-11-12 16:04:43 +00:00
**Last Updated: ** November 9, 2025
**API Version: ** 1.0.0
2025-11-09 13:54:31 +00:00
**Documentation Version: ** 1.0.0