# CV Site API Documentation **Note**: This is my personal CV website API documentation. While the code is open-source (MIT license), this API is designed for my personal site and may change without notice. ## Overview This API provides endpoints for a bilingual (English/Spanish) CV/resume web application built with Go's standard library. The API supports both traditional HTTP requests and HTMX-powered partial page updates for a seamless single-page application experience. **Version:** 1.0.0 **Base URL:** `http://localhost:1999` (development) **Default Port:** 1999 (configurable via `PORT` environment variable) ### Key Features - 🌍 **Bilingual Support**: English and Spanish content switching - ⚑ **HTMX-Aware**: Serves partial HTML for HTMX requests - πŸ“„ **PDF Export**: Generate PDF resumes using headless Chrome - πŸ”’ **Security Headers**: Production-grade security headers (CSP, HSTS, etc.) - πŸ“Š **Health Monitoring**: JSON health check endpoint - 🎯 **No Authentication**: Public CV/resume site ### Content Types - **HTML**: `text/html; charset=utf-8` (default) - **JSON**: `application/json` (health endpoint, errors) - **PDF**: `application/pdf` (export endpoint) ### Configuration The server can be configured via environment variables: | Variable | Default | Description | |----------|---------|-------------| | `PORT` | `1999` | Server port | | `HOST` | `localhost` | Server host | | `READ_TIMEOUT` | `15` | Read timeout in seconds | | `WRITE_TIMEOUT` | `15` | Write timeout in seconds | | `GO_ENV` | `development` | Environment mode (`development`/`production`) | | `TEMPLATE_HOT_RELOAD` | `true` (dev) | Enable template hot reloading | --- ## Quick Reference **Quick access to common operations and endpoints.** ### Base URL ``` http://localhost:1999 ``` ### All Endpoints | Endpoint | Method | Description | Common Use | |----------|--------|-------------|------------| | `/?lang={en\|es}` | GET | Full HTML page with CV content | Initial page load | | `/cv?lang={en\|es}` | GET | HTML partial for HTMX swaps | Language switching | | `/export/pdf?lang={en\|es}&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" # Static file with headers curl -I http://localhost:1999/static/css/main.css ``` ### HTMX Integration Pattern ```html
``` ### Common Error Codes | Code | Meaning | Common Cause | |------|---------|--------------| | 200 | Success | Request processed correctly | | 400 | Bad Request | Invalid `lang` parameter (not `en` or `es`) | | 403 | Forbidden | Origin check failed (PDF endpoint) | | 404 | Not Found | Invalid route or static file not found | | 429 | Too Many Requests | Rate limit exceeded (PDF endpoint) | | 500 | Server Error | Template error, data loading error, PDF generation failed | ### Performance Targets | Endpoint | Target Response Time | |----------|---------------------| | `/health` | <1ms | | `/` and `/cv` | 7-8ms | | `/static/*` | <5ms | | `/export/pdf` | ~3 seconds | ### Environment Configuration ```bash # Development PORT=1999 HOST=localhost GO_ENV=development # Production PORT=1999 HOST=0.0.0.0 GO_ENV=production ALLOWED_ORIGINS=yourdomain.com,www.yourdomain.com ``` ### Need More Details? For comprehensive documentation of each endpoint, request/response formats, and advanced usage, see the [Detailed Endpoint Documentation](#detailed-endpoint-documentation) below. --- ## Architecture ### Route Organization All routes and middleware configuration are centralized in `internal/routes/routes.go` for clean separation of concerns: ```go // internal/routes/routes.go func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler { mux := http.NewServeMux() // Public routes mux.HandleFunc("/", cvHandler.Home) mux.HandleFunc("/cv", cvHandler.CVContent) mux.HandleFunc("/health", healthHandler.Check) // Protected PDF endpoint (rate limited + origin checked) // Static files with cache control // Middleware chain (Recovery β†’ Logger β†’ SecurityHeaders) return handler } ``` This architecture provides: - βœ… Single source of truth for routes - βœ… Clear middleware chain visibility - βœ… Easy route management and testing - βœ… Clean separation from main.go ### Data Loading **Simplified Architecture** (cache removed as of v1.1.0): - JSON files loaded directly from disk on each request - No caching layer (over-engineering for static CV data) - Go's built-in file system caching is sufficient - Data only changes on deployment/restart ## Endpoints Overview | Method | Path | Description | HTMX Support | Protection | |--------|------|-------------|--------------|------------| | GET | `/` | Full CV page (home) | ❌ No | None | | GET | `/cv` | CV content partial | βœ… Yes | None | | GET | `/export/pdf` | PDF export | ❌ No | βœ… Rate Limited + Origin Check | | GET | `/health` | Health check | ❌ No | None | | GET | `/static/*` | Static files (CSS, JS, images) | ❌ No | Cache Control | --- ## Detailed Endpoint Documentation ### 1. GET / **Description:** Renders the complete CV page with full HTML structure including header, navigation, and footer. #### Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `lang` | string | No | `en` | Language code (`en` or `es`) | #### Request Headers No special headers required. #### Response **Status Code:** `200 OK` **Content-Type:** `text/html; charset=utf-8` **Response Body:** Complete HTML document with: - ``, ``, `` tags - Navigation and header - Full CV content - Footer - Embedded styles and scripts #### Examples **curl - English CV:** ```bash curl http://localhost:1999/ ``` **curl - Spanish CV:** ```bash curl http://localhost:1999/?lang=es ``` **Browser:** ``` http://localhost:1999/?lang=en ``` #### Error Responses **400 Bad Request** - Invalid language parameter: ```http HTTP/1.1 400 Bad Request Content-Type: text/plain Unsupported language. Use 'en' or 'es' ``` **500 Internal Server Error** - Template or data loading error: ```http HTTP/1.1 500 Internal Server Error Content-Type: text/plain Internal Server Error ``` #### Notes - This endpoint always returns the full page structure - Ideal for initial page load and direct navigation - Not optimized for HTMX partial updates (use `/cv` instead) - Calculates dynamic experience duration and years of experience - Fetches git repository dates for projects (if available) --- ### 2. GET /cv **Description:** Renders only the CV content section for HTMX partial page swaps. Returns the same content as `/` but without the HTML wrapper. #### Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `lang` | string | No | `en` | Language code (`en` or `es`) | #### Request Headers | Header | Value | Description | |--------|-------|-------------| | `HX-Request` | `true` | Indicates HTMX request (optional, but recommended) | #### Response **Status Code:** `200 OK` **Content-Type:** `text/html; charset=utf-8` **Response Body:** CV content HTML fragment (no ``, ``, or `` 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 ``` **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
Unsupported language. Use 'en' or 'es'
``` **500 Internal Server Error** - Template error: ```http HTTP/1.1 500 Internal Server Error Content-Type: text/html
An error occurred. Please try again later.
``` #### 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 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 Download CV (PDF) ``` **HTMX Button (triggers download):** ```html ``` #### 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 Unsupported language. Use 'en' or 'es' ``` **400 Bad Request** - Invalid length: ```http HTTP/1.1 400 Bad Request Content-Type: text/plain Unsupported length. Use 'short' or 'long' ``` **400 Bad Request** - Invalid icons option: ```http HTTP/1.1 400 Bad Request Content-Type: text/plain Unsupported icons option. Use 'show' or 'hide' ``` **400 Bad Request** - Invalid version: ```http HTTP/1.1 400 Bad Request Content-Type: text/plain Unsupported version. Use 'long' or 'clean' ``` **500 Internal Server Error** - PDF generation failed: ```http HTTP/1.1 500 Internal Server Error Content-Type: text/plain Internal Server Error ``` #### 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 /* CSS content */ ``` **HTML Integration:** ```html Logo ``` #### Error Responses **404 Not Found:** ```http HTTP/1.1 404 Not Found Content-Type: text/plain 404 page not found ``` #### Notes - **Standard Library:** Uses `http.FileServer` from Go stdlib - **Path Stripping:** `/static/` prefix is stripped before file lookup - **Directory Listing:** Disabled (shows 403 if directory accessed) - **Cache Control:** Environment-aware caching strategy - **Security:** Served through security middleware (CSP, X-Content-Type-Options) - **No Compression:** Enable gzip compression in reverse proxy (nginx/Caddy) for production --- ## HTMX Integration ### Overview The application is designed to work seamlessly with HTMX for dynamic, partial page updates without full page reloads. The primary use case is language switching. ### HTMX Detection The server detects HTMX requests via the `HX-Request` header: ```go isHTMX := r.Header.Get("HX-Request") != "" ``` ### Response Behavior | Endpoint | HTMX Header | Response Type | |----------|-------------|---------------| | `/` | Any | Full HTML page | | `/cv` | Present | HTML fragment | | `/cv` | Absent | HTML fragment | | `/export/pdf` | Any | PDF binary | | `/health` | Any | JSON | ### Language Switching Pattern **HTML Structure:** ```html
``` **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
Unsupported language. Use 'en' or 'es'
``` **HTMX Error Handling:** ```html
Loading...
``` ### Out-of-Band Swaps Currently not implemented, but could be used for updating multiple page sections: ```html

CV - Spanish

``` --- ## 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
Unsupported language. Use 'en' or 'es'
``` #### 3. Standard HTTP Errors (default) **Request:** ```bash curl "http://localhost:1999/?lang=invalid" ``` **Response:** ```http HTTP/1.1 400 Bad Request Content-Type: text/plain Unsupported language. Use 'en' or 'es' ``` ### Common Error Codes | Status Code | Error Type | When It Occurs | |-------------|------------|----------------| | 400 | Bad Request | Invalid language parameter | | 404 | Not Found | Static file not found, invalid route | | 500 | Internal Server Error | Template rendering error, data loading error, PDF generation error | ### Error Types (Internal) ```go // Defined in internal/handlers/errors.go BadRequestError() // 400 - Client error NotFoundError() // 404 - Resource not found InternalError() // 500 - Server error (details hidden) TemplateError() // 500 - Template rendering failed DataLoadError() // 500 - Failed to load CV data ``` ### Internal vs Public Errors **Internal Errors** (500): - Details logged server-side only - Generic message returned to client: `"Internal Server Error"` - Includes: template errors, data loading errors, PDF generation errors **Public Errors** (4xx): - Descriptive message returned to client - Details safe to expose - Includes: invalid language, not found ### Error Logging All errors are logged server-side with context: ``` ERROR [GET /]: Error rendering template: index.html CLIENT ERROR [GET /]: Unsupported language. Use 'en' or 'es' (status: 400) ``` ### Error Handling Best Practices **Client-Side:** ```javascript // Fetch with error handling async function loadCV(lang) { try { const response = await fetch(`/cv?lang=${lang}`); if (!response.ok) { const error = await response.json(); console.error('API Error:', error.message); return; } const html = await response.text(); document.getElementById('cv-content').innerHTML = html; } catch (err) { console.error('Network Error:', err); alert('Failed to connect to server'); } } ``` **HTMX Error Handling:** ```html ``` --- ## Performance & Caching ### Cache Headers Strategy #### Static Files (CSS, JS, Images) **Development:** ```http Cache-Control: public, max-age=3600 ``` - 1 hour cache - Allows rapid development without stale cache issues **Production:** ```http Cache-Control: public, max-age=86400 ``` - 1 day cache - Reduces server load and bandwidth - Improves page load performance #### Dynamic Content (HTML) No cache headers set (browser default behavior): - CV content changes rarely - Language switching requires fresh content - Could add `Cache-Control: private, max-age=300` for 5-minute cache #### PDF Export No cache headers: - Generated on-demand - Content-Disposition triggers download - Browser doesn't cache downloads by default ### Template Caching **Development Mode:** ```go HotReload: true // Templates reloaded on every request ``` **Production Mode:** ```go HotReload: false // Templates compiled once on startup ``` ### Performance Metrics | Endpoint | Avg Response Time | Notes | |----------|-------------------|-------| | `/` | 5-15ms | Template rendering + data loading | | `/cv` | 5-15ms | Same as `/` (uses same logic) | | `/export/pdf` | 2-5 seconds | Headless Chrome rendering | | `/health` | <1ms | Simple JSON response | | `/static/*` | <5ms | Direct file serving | ### Optimization Recommendations #### 1. Add Response Compression Use reverse proxy (nginx/Caddy) for gzip compression: **nginx:** ```nginx gzip on; gzip_types text/html text/css application/javascript application/json; gzip_min_length 1000; ``` #### 2. Add ETag Support ```go // Add to static file handler w.Header().Set("ETag", `"`+fileHash+`"`) ``` #### 3. Add Conditional Requests ```go if match := r.Header.Get("If-None-Match"); match == etag { w.WriteHeader(http.StatusNotModified) return } ``` #### 4. Implement HTTP/2 ```go // Use TLS for HTTP/2 support server.ListenAndServeTLS("cert.pem", "key.pem") ``` #### 5. PDF Generation Optimization ```go // Cache generated PDFs for 5 minutes pdfCache := cache.New(5*time.Minute, 10*time.Minute) ``` ### Resource Limits **Timeouts:** ```go ReadTimeout: 15 seconds // Request read timeout WriteTimeout: 15 seconds // Response write timeout IdleTimeout: 120 seconds // Keep-alive timeout ``` **PDF Generation:** ```go PDFTimeout: 30 seconds // Chromedp context timeout ``` --- ## Security ### Security Headers All responses include production-grade security headers via middleware: ```http X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()... Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'... ``` **Production Only:** ```http Strict-Transport-Security: max-age=31536000; includeSubDomains; preload ``` ### Content Security Policy (CSP) ``` default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://code.iconify.design; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.iconify.design; frame-ancestors 'self'; base-uri 'self'; form-action 'self' ``` **External Resources Allowed:** - HTMX from unpkg.com - Icons from iconify.design - Fonts from Google Fonts ### CORS Not explicitly configured (same-origin only): - No `Access-Control-Allow-Origin` headers - API not designed for cross-origin access - Could be added if needed for external integrations ### Authentication **Current State:** None This is a public CV/resume site with no authentication or authorization. **Future Considerations:** - Add admin authentication for CV data updates - Implement API key for PDF export rate limiting - Add OAuth for private CV access ### Input Validation **Language Parameter:** ```go if lang != "en" && lang != "es" { return BadRequestError("Unsupported language. Use 'en' or 'es'") } ``` **Path Traversal Protection:** - Static file handler uses `http.FileServer` (built-in protection) - No user-controlled file paths - No database queries (no SQL injection risk) ### Origin Checking & API Protection **Status:** βœ… **Implemented** The API implements origin checking to prevent external sites from hotlinking to resource-intensive endpoints like PDF generation. **Protected Endpoints:** - `/export/pdf` - Full protection (origin checking + rate limiting) **Configuration via Environment Variable:** ```bash # Development (default) ALLOWED_ORIGINS= # Production ALLOWED_ORIGINS=yourdomain.com,www.yourdomain.com ``` **How It Works:** 1. Checks `Origin` header (CORS requests) 2. Falls back to `Referer` header (navigation requests) 3. Allows localhost in development 4. Blocks external domains in production 5. Requires headers in production for PDF endpoint **Example Requests:** ```bash # βœ… Allowed (localhost in development) curl http://localhost:1999/export/pdf?lang=en # βœ… Allowed (valid referer) curl -H "Referer: http://localhost:1999/" \ http://localhost:1999/export/pdf?lang=en # ❌ Blocked (external referer) curl -H "Referer: https://evil.com/" \ http://localhost:1999/export/pdf?lang=en # Response: 403 Forbidden ``` For more details on origin checking, see [9-SECURITY.md](9-SECURITY.md#origin-checking). ### Rate Limiting **Status:** βœ… **Implemented** **Current Configuration:** - **Endpoint:** `/export/pdf` - **Limit:** 3 requests per minute per IP - **Window:** 1 minute (rolling) - **Response:** 429 Too Many Requests when exceeded **Implementation:** ```go // Applied in main.go pdfRateLimiter := middleware.NewRateLimiter(3, 1*time.Minute) protectedPDFHandler := middleware.OriginChecker( pdfRateLimiter.Middleware( http.HandlerFunc(cvHandler.ExportPDF), ), ) ``` **Behavior:** | Requests | Status | Response | |----------|--------|----------| | 1st-3rd requests | βœ… 200 OK | PDF generated | | 4th+ request (within 1 min) | ❌ 429 Too Many Requests | Rate limit exceeded | | After 1 minute | βœ… 200 OK | Counter reset | **Rate Limit Response:** ```http HTTP/1.1 429 Too Many Requests Retry-After: 60 Content-Type: text/plain; charset=utf-8 Rate limit exceeded. Please try again later. ``` **IP Detection:** - Checks `X-Forwarded-For` (proxy/CDN) - Falls back to `X-Real-IP` (alternative proxy header) - Uses `RemoteAddr` (direct connection) - Works with Nginx reverse proxy **Testing Rate Limit:** ```bash # Generate 4 PDFs quickly to test rate limiting for i in {1..4}; do echo "Request $i:" curl -w "Status: %{http_code}\n" -o /dev/null -s \ http://localhost:1999/export/pdf?lang=en sleep 1 done # Expected output: # Request 1: Status: 200 # Request 2: Status: 200 # Request 3: Status: 200 # Request 4: Status: 429 ``` **Customizing Rate Limits:** Edit `main.go` to adjust limits: ```go // More restrictive: 5 per hour pdfRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour) // Less restrictive: 10 per minute pdfRateLimiter := middleware.NewRateLimiter(10, 1*time.Minute) ``` For comprehensive protection documentation, see [9-SECURITY.md](9-SECURITY.md#api-protection). ### Security Best Practices βœ… **Implemented:** - Security headers (CSP, X-Frame-Options, etc.) - HSTS in production - Input validation - Error message sanitization (internal errors hidden) - Timeouts on all operations - Graceful shutdown - Origin checking (prevents external hotlinking) - Rate limiting (PDF endpoint: 3 requests/min per IP) - IP-based tracking (supports reverse proxies) ⚠️ **Recommended for Production:** - Use HTTPS (prevents header spoofing) - Configure `ALLOWED_ORIGINS` for your domain - Implement request logging with IP addresses - Add monitoring and alerting for 403/429 responses - Consider CloudFlare for additional DDoS protection - Set up log retention for security analysis --- ## Rate Limiting **Current State:** βœ… **Fully Implemented** (as of v1.1.0) ### Current Implementation Rate limiting is configured in `internal/routes/routes.go` and applied via middleware: #### Per-Endpoint Limits | Endpoint | Recommended Limit | Burst | |----------|-------------------|-------| | `/` | 20 req/min | 10 | | `/cv` | 30 req/min | 15 | | `/export/pdf` | 5 req/min | 2 | | `/health` | Unlimited | - | | `/static/*` | 100 req/min | 50 | #### Implementation Options **1. Nginx (Recommended for Production):** ```nginx # Define rate limit zones limit_req_zone $binary_remote_addr zone=general:10m rate=20r/m; limit_req_zone $binary_remote_addr zone=pdf:10m rate=5r/m; server { location / { limit_req zone=general burst=10 nodelay; } location /cv { limit_req zone=general burst=15 nodelay; } location /export/pdf { limit_req zone=pdf burst=2 nodelay; limit_req_status 429; } } ``` **2. Go Middleware (for standalone deployment):** ```go import ( "golang.org/x/time/rate" "sync" ) type IPRateLimiter struct { ips map[string]*rate.Limiter mu *sync.RWMutex r rate.Limit b int } func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter { return &IPRateLimiter{ ips: make(map[string]*rate.Limiter), mu: &sync.RWMutex{}, r: r, b: b, } } func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter { i.mu.Lock() defer i.mu.Unlock() limiter, exists := i.ips[ip] if !exists { limiter = rate.NewLimiter(i.r, i.b) i.ips[ip] = limiter } return limiter } func RateLimitMiddleware(limiter *IPRateLimiter) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := r.RemoteAddr limiter := limiter.GetLimiter(ip) if !limiter.Allow() { http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) } } ``` **3. Cloudflare Rate Limiting (easiest):** - Configure in Cloudflare dashboard - No code changes required - Enterprise-grade DDoS protection --- ## Examples & Use Cases ### Use Case 1: Language Switching with HTMX **Scenario:** User wants to switch between English and Spanish CV without page reload. **Implementation:** ```html My CV
``` --- ### Use Case 2: PDF Export with Progress Indicator **Scenario:** User clicks "Download PDF" and sees progress while PDF generates. **Implementation:** ```html ``` --- ### 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 ``` --- ### Use Case 5: Curl Testing Suite **Complete API testing with curl:** ```bash #!/bin/bash # test-api.sh - Complete API test suite API_URL="http://localhost:1999" PASSED=0 FAILED=0 test_endpoint() { name=$1 url=$2 expected_status=$3 echo "Testing: $name" status=$(curl -s -o /dev/null -w "%{http_code}" "$url") if [ "$status" -eq "$expected_status" ]; then echo " βœ… PASS (HTTP $status)" ((PASSED++)) else echo " ❌ FAIL (Expected $expected_status, got $status)" ((FAILED++)) fi } echo "=== CV API Test Suite ===" echo # Test 1: Health check test_endpoint "Health Check" "$API_URL/health" 200 # Test 2: Home page (English) test_endpoint "Home - English" "$API_URL/?lang=en" 200 # Test 3: Home page (Spanish) test_endpoint "Home - Spanish" "$API_URL/?lang=es" 200 # Test 4: Invalid language test_endpoint "Invalid Language" "$API_URL/?lang=fr" 400 # Test 5: CV content endpoint test_endpoint "CV Content" "$API_URL/cv?lang=en" 200 # Test 6: Static CSS test_endpoint "Static CSS" "$API_URL/static/css/main.css" 200 # Test 7: PDF export (takes time) echo "Testing: PDF Export (this may take a few seconds)" pdf_status=$(curl -s -o /tmp/test-cv.pdf -w "%{http_code}" "$API_URL/export/pdf?lang=en") if [ "$pdf_status" -eq 200 ] && [ -s /tmp/test-cv.pdf ]; then pdf_size=$(wc -c < /tmp/test-cv.pdf) echo " βœ… PASS (HTTP $pdf_status, PDF size: $pdf_size bytes)" ((PASSED++)) rm /tmp/test-cv.pdf else echo " ❌ FAIL (HTTP $pdf_status or empty file)" ((FAILED++)) fi # Test 8: 404 Not Found test_endpoint "404 Not Found" "$API_URL/nonexistent" 404 # Test 9: Health response format echo "Testing: Health Response Format" health=$(curl -s "$API_URL/health") if echo "$health" | jq -e '.status == "ok"' > /dev/null 2>&1; then echo " βœ… PASS (Valid JSON with status:ok)" ((PASSED++)) else echo " ❌ FAIL (Invalid JSON or missing status)" ((FAILED++)) fi # Summary echo echo "=== Test Summary ===" echo "Passed: $PASSED" echo "Failed: $FAILED" echo "Total: $((PASSED + FAILED))" if [ $FAILED -eq 0 ]; then echo "βœ… All tests passed!" exit 0 else echo "❌ Some tests failed" exit 1 fi ``` **Run tests:** ```bash chmod +x test-api.sh ./test-api.sh ``` --- ## Troubleshooting ### Common Issues #### Issue 1: Server Won't Start **Symptom:** ``` ❌ Failed to initialize templates: open templates/index.html: no such file or directory ``` **Solution:** ```bash # Ensure you're in the project root cd /path/to/cv-site # Check template directory exists ls -la templates/ # Check file permissions chmod 644 templates/*.html ``` --- #### Issue 2: PDF Generation Fails **Symptom:** ``` PDF generation failed: chrome not found ``` **Solution:** ```bash # macOS brew install --cask chromium # Ubuntu/Debian sudo apt-get install chromium-browser # Verify installation which chromium ``` **Alternative:** Use Docker with Chrome pre-installed: ```dockerfile FROM golang:1.21-alpine RUN apk add chromium ``` --- #### Issue 3: Language Switch Not Working **Symptom:** Clicking language buttons doesn't change content **Diagnosis:** ```bash # Check HTMX is loaded curl -I http://localhost:1999/static/js/htmx.min.js # Test endpoint directly curl "http://localhost:1999/cv?lang=es" ``` **Solution:** - Verify HTMX script tag in HTML - Check browser console for JavaScript errors - Ensure `hx-target` matches actual element ID --- #### Issue 4: Static Files Not Loading **Symptom:** CSS/JS 404 errors **Diagnosis:** ```bash # Check static directory ls -la static/css/ ls -la static/js/ # Test endpoint curl -I http://localhost:1999/static/css/main.css ``` **Solution:** ```bash # Create missing directories mkdir -p static/css static/js static/images # Check file paths in HTML match actual paths grep -r 'href="/static' templates/ ``` --- #### Issue 5: Slow PDF Generation **Symptom:** PDF export takes >10 seconds **Diagnosis:** ```bash # Time the request time curl -o test.pdf "http://localhost:1999/export/pdf?lang=en" ``` **Solutions:** 1. **Optimize CSS:** Remove unused styles 2. **Reduce images:** Compress or lazy-load images 3. **Cache PDFs:** Implement 5-minute cache 4. **Increase timeout:** Adjust chromedp timeout 5. **Use worker pool:** Queue PDF generation requests --- ### Debug Mode Enable verbose logging: ```bash # Set log flags export GO_ENV=development export LOG_LEVEL=debug # Run server go run main.go ``` **Logs will show:** ``` [GET] /cv?lang=es 127.0.0.1:54321 - 200 (12.5ms) [GET] /export/pdf?lang=en 127.0.0.1:54322 - 200 (3.2s) ERROR [GET /]: Error loading CV data ``` --- ### Performance Profiling **1. Enable pprof:** ```go import _ "net/http/pprof" // In main() go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() ``` **2. Profile CPU:** ```bash go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 ``` **3. Profile Memory:** ```bash go tool pprof http://localhost:6060/debug/pprof/heap ``` **4. View traces:** ```bash curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out go tool trace trace.out ``` --- ## Appendix ### Environment Variables Reference | Variable | Type | Default | Description | |----------|------|---------|-------------| | `PORT` | int | `1999` | Server listen port | | `HOST` | string | `localhost` | Server bind address | | `GO_ENV` | string | `development` | Environment mode | | `READ_TIMEOUT` | int | `15` | Request read timeout (seconds) | | `WRITE_TIMEOUT` | int | `15` | Response write timeout (seconds) | | `TEMPLATE_DIR` | string | `templates` | Template directory path | | `PARTIALS_DIR` | string | `templates/partials` | Partial templates path | | `TEMPLATE_HOT_RELOAD` | bool | `true` (dev) | Enable template hot reload | | `DATA_DIR` | string | `data` | CV data directory | ### Response Time Targets | Endpoint | Target | Acceptable | Action Required | |----------|--------|------------|-----------------| | `/` | <50ms | <200ms | >200ms investigate | | `/cv` | <50ms | <200ms | >200ms investigate | | `/health` | <10ms | <50ms | >50ms investigate | | `/static/*` | <20ms | <100ms | >100ms add CDN | | `/export/pdf` | <3s | <5s | >5s optimize | ### HTTP Status Code Reference | Code | Name | When Used | |------|------|-----------| | 200 | OK | Successful request | | 304 | Not Modified | Cached resource (ETag match) | | 400 | Bad Request | Invalid language parameter | | 404 | Not Found | Route or static file not found | | 429 | Too Many Requests | Rate limit exceeded (if implemented) | | 500 | Internal Server Error | Template, data, or PDF error | | 503 | Service Unavailable | Server shutting down | ### Version History | Version | Date | Changes | |---------|------|---------| | 1.1.0 | 2025-11-12 | Route extraction to internal/routes, cache removal, rate limiting implementation | | 1.0.0 | 2025-11-09 | Initial release with Go rewrite | ### Related Documentation - [README.md](../README.md) - Project overview and setup - [CONTRIBUTING.md](../CONTRIBUTING.md) - Contribution guidelines - [8-DEPLOYMENT.md](8-DEPLOYMENT.md) - Deployment guides ### Support **Issues:** [GitHub Issues](https://github.com/juanatsap/cv-site/issues) **Email:** [juan.a.moreno.rubio@gmail.com](mailto:juan.a.moreno.rubio@gmail.com) --- **Last Updated:** November 12, 2025 **API Version:** 1.1.0 **Documentation Version:** 1.1.0