# Go Routes and API Documentation ## Overview The CV site uses Go's standard `net/http` package with a custom routing setup in `internal/routes/routes.go`. The routing system applies a comprehensive middleware chain for security, logging, caching, and preferences management. ### Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Middleware Chain │ │ │ │ Request → Recovery → Logger → SecurityHeaders │ │ ↓ ↓ ↓ │ │ DynamicCache → Preferences → Router → Handler │ │ ↓ │ │ ┌───────────────┐ │ │ │ Mux Routes │ │ │ │ - Public │ │ │ │ - HTMX │ │ │ │ - API │ │ │ │ - Protected │ │ │ └───────────────┘ │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ Protected Endpoints │ │ │ │ /api/contact: │ │ BrowserOnly → RateLimiter(5/hour) → Handler │ │ │ │ /export/pdf: │ │ OriginChecker → RateLimiter(3/min) → Handler │ │ │ │ /static/*: │ │ CacheControl → FileServer │ └─────────────────────────────────────────────────────────────┘ ``` ## Route Table ### Public Routes | Route | Method | Handler | Description | Response Type | |-------|--------|---------|-------------|---------------| | `/` | GET | `cvHandler.Home` | Main CV page with full layout | HTML | | `/cv` | GET | `cvHandler.CVContent` | CV content partial (HTMX) | HTML (partial) | | `/text` | GET | `cvHandler.PlainText` | Plain text CV for curl/AI | text/plain | | `/health` | GET | `healthHandler.Check` | Health check endpoint | JSON | #### `/` - Home Page **Purpose:** Serves the main CV page with full HTML layout **Handler:** `cvHandler.Home` **Middleware:** - Recovery - Logger - SecurityHeaders - DynamicCacheControl - PreferencesMiddleware **Response:** ```html Juan's CV ``` **Status Codes:** - `200 OK` - Success - `500 Internal Server Error` - Template error --- #### `/cv` - CV Content Partial **Purpose:** Returns CV content fragment for HTMX swaps **Handler:** `cvHandler.CVContent` **Query Parameters:** - `lang` - Language (en/es) - `length` - CV length (short/full) - `icons` - Show icons (true/false) **Response:** ```html
...
...
``` **HTMX Usage:** ```html ``` --- #### `/text` - Plain Text CV **Purpose:** curl-friendly and AI-readable plain text CV **Handler:** `cvHandler.PlainText` **Response:** ``` JUAN ANDRÉS MORENO RUBIO ======================== Software Engineer | Go, HTMX, Cloud Architecture EXPERIENCE ---------- Senior Software Engineer @ Company (2020-Present) - Achievement 1 - Achievement 2 ... ``` **Usage:** ```bash curl https://juan.andres.morenorub.io/text ``` **Status Codes:** - `200 OK` - Success - `500 Internal Server Error` - Generation error --- #### `/health` - Health Check **Purpose:** Service health monitoring for load balancers **Handler:** `healthHandler.Check` **Response:** ```json { "status": "healthy", "timestamp": "2025-12-06T10:30:00Z", "version": "1.0.0" } ``` **Status Codes:** - `200 OK` - Service healthy - `503 Service Unavailable` - Service degraded ### HTMX Interactive Endpoints | Route | Method | Handler | Description | Response Type | |-------|--------|---------|-------------|---------------| | `/switch-language` | GET | `cvHandler.SwitchLanguage` | Toggle EN/ES language | HTML (partial) | | `/toggle/length` | GET | `cvHandler.ToggleLength` | Toggle short/full CV | HTML (partial) | | `/toggle/icons` | GET | `cvHandler.ToggleIcons` | Toggle icon display | HTML (partial) | | `/toggle/theme` | GET | `cvHandler.ToggleTheme` | Toggle light/dark theme | HTML (partial) | #### `/switch-language` - Language Toggle **Purpose:** Switch between English and Spanish **Handler:** `cvHandler.SwitchLanguage` **Mechanism:** 1. Reads current language from preferences cookie 2. Toggles `en` ↔ `es` 3. Sets new preference cookie 4. Returns updated CV content **Response:** ```html
``` **HTMX Trigger:** ```html ``` --- #### `/toggle/length` - CV Length Toggle **Purpose:** Switch between short (1-page) and full (detailed) CV **Handler:** `cvHandler.ToggleLength` **States:** - `short` - Essential experience only (1 page) - `full` - Complete experience history **Response:** ```html
``` **Usage:** ```html ``` --- #### `/toggle/icons` - Icon Display Toggle **Purpose:** Show/hide skill and technology icons **Handler:** `cvHandler.ToggleIcons` **States:** - `true` - Show icons (visual) - `false` - Hide icons (text only, PDF-friendly) **Response:** ```html
``` --- #### `/toggle/theme` - Theme Toggle **Purpose:** Switch between light and dark mode **Handler:** `cvHandler.ToggleTheme` **States:** - `light` - Light theme - `dark` - Dark theme **Response:** ```html ``` **HTMX Trigger:** ```html ``` ### API Endpoints | Route | Method | Handler | Description | Security | |-------|--------|---------|-------------|----------| | `/api/cmd-k` | GET | `cvHandler.CmdKData` | Command palette data | Standard chain | | `/api/contact` | POST | `cvHandler.HandleContact` | Contact form submission | BrowserOnly + RateLimit(5/hour) | #### `/api/cmd-k` - Command Palette Data **Purpose:** Provides search data for CMD+K command palette **Handler:** `cvHandler.CmdKData` **Response:** ```json { "commands": [ { "id": "view-experience", "title": "View Experience", "description": "Jump to experience section", "action": "#experience" }, { "id": "download-pdf", "title": "Download PDF", "description": "Export CV as PDF", "action": "/export/pdf" }, { "id": "switch-language", "title": "Switch to Spanish", "description": "Change language to Spanish", "action": "/switch-language" } ] } ``` **Status Codes:** - `200 OK` - Success --- #### `/api/contact` - Contact Form Submission **Purpose:** Handle contact form submissions with comprehensive security **Handler:** `cvHandler.HandleContact` **Method:** `POST` **Security Chain:** ``` BrowserOnly → RateLimiter(5/hour) → Handler ``` **Request Body:** ```json { "name": "Juan José", "email": "juan@example.com", "company": "ACME Corp", "subject": "Job Opportunity", "message": "I'd like to discuss...", "website": "", "timestamp": 1701867000 } ``` **Validation Rules:** - `name`: Required, max 100 chars, letters/spaces/hyphens only - `email`: Required, max 254 chars, valid RFC 5322 email - `company`: Optional, max 100 chars - `subject`: Required, max 200 chars - `message`: Required, max 5000 chars - `website`: Honeypot (must be empty) - `timestamp`: Must be 2s-24h old **Success Response (200 OK):** ```json { "success": true, "message": "Message sent successfully" } ``` **Error Response (400 Bad Request):** ```json { "success": false, "errors": [ { "field": "email", "message": "Invalid email address format" } ] } ``` **Error Response (429 Too Many Requests):** ```html

Too Many Requests

You've submitted too many contact forms. Please wait an hour before trying again.

``` **Status Codes:** - `200 OK` - Success - `400 Bad Request` - Validation error - `403 Forbidden` - BrowserOnly check failed - `429 Too Many Requests` - Rate limit exceeded - `500 Internal Server Error` - Email send failure **Security Features:** 1. **BrowserOnly Middleware:** - Blocks curl, Postman, wget - Requires User-Agent, Referer/Origin - Requires HTMX or XMLHttpRequest headers 2. **Rate Limiting:** - 5 submissions per hour per IP - 1-hour window - Automatic cleanup 3. **Validation:** - Email header injection prevention - Honeypot bot detection - Timing-based bot detection - HTML sanitization ### Protected PDF Endpoint | Route | Method | Handler | Description | Security | |-------|--------|---------|-------------|----------| | `/export/pdf` | GET | `cvHandler.ExportPDF` | Generate and download PDF | OriginChecker + RateLimit(3/min) | #### `/export/pdf` - PDF Export **Purpose:** Generate and serve CV as PDF **Handler:** `cvHandler.ExportPDF` **Security Chain:** ``` OriginChecker → RateLimiter(3/min) → Handler ``` **Query Parameters:** - `lang` - Language (en/es) - default: current preference - `length` - CV length (short/full) - default: current preference - `icons` - Show icons (true/false) - default: true **Response Headers:** ``` Content-Type: application/pdf Content-Disposition: attachment; filename="cv-jamr-2025-en.pdf" Cache-Control: no-cache, no-store, must-revalidate ``` **Status Codes:** - `200 OK` - PDF generated successfully - `403 Forbidden` - Origin check failed or direct access blocked - `429 Too Many Requests` - Rate limit exceeded (3/min) - `500 Internal Server Error` - PDF generation failed **OriginChecker:** - Checks `Origin` or `Referer` header - Allows: `juan.andres.morenorub.io`, `localhost`, `127.0.0.1` - Blocks external site hotlinking - In production: Blocks direct access (requires referer) **RateLimiter:** - Limit: 3 requests per minute per IP - Window: 60 seconds - Automatic entry cleanup **Usage:** ```html Download Short CV (English) ``` ### PDF Shortcut Routes | Route | Method | Handler | Description | |-------|--------|---------|-------------| | `/cv-jamr-*` | GET | `cvHandler.DefaultCVShortcut` | Year-aware PDF shortcuts | #### `/cv-jamr-*` - Default CV Shortcuts **Purpose:** Friendly URLs for direct PDF access **Handler:** `cvHandler.DefaultCVShortcut` **Pattern:** `/cv-jamr-{year}-{lang}.pdf` **Examples:** - `/cv-jamr-2025-en.pdf` - English CV for 2025 - `/cv-jamr-2025-es.pdf` - Spanish CV for 2025 - `/cv-jamr-2024-en.pdf` - English CV for 2024 **Behavior:** 1. Parse year and language from URL 2. Redirect to `/export/pdf?lang={lang}&length=full` 3. Set appropriate filename in response **Status Codes:** - `200 OK` - PDF served successfully - `302 Found` - Redirect to export endpoint - `400 Bad Request` - Invalid URL format ### Static Files | Route | Method | Handler | Description | Middleware | |-------|--------|---------|-------------|------------| | `/static/*` | GET | FileServer | CSS, JS, images, fonts | CacheControl | #### `/static/*` - Static Assets **Purpose:** Serve static files (CSS, JavaScript, images, fonts) **Handler:** `http.FileServer(http.Dir("static"))` **Directory Structure:** ``` static/ ├── css/ │ ├── main.css │ └── themes/ │ ├── light.css │ └── dark.css ├── js/ │ ├── app.js │ └── htmx.min.js ├── images/ │ ├── logo.png │ └── avatar.jpg ├── fonts/ │ └── inter.woff2 └── icons/ └── sprites.svg ``` **Cache Headers:** ``` # Development Cache-Control: public, max-age=3600 # 1 hour # Production Cache-Control: public, max-age=86400 # 1 day ``` **Middleware:** `CacheControl` **Examples:** ``` /static/css/main.css /static/js/htmx.min.js /static/images/avatar.jpg /static/fonts/inter.woff2 /static/icons/sprites.svg ``` ## Middleware Stack ### Global Middleware Chain Applied to **all routes** in this order: ```go handler := middleware.Recovery( middleware.Logger( middleware.SecurityHeaders( middleware.DynamicCacheControl( middleware.PreferencesMiddleware(mux), ), ), ), ) ``` #### 1. Recovery **Purpose:** Panic recovery and graceful error handling **File:** `internal/middleware/recovery.go` **Behavior:** - Catches panics in handlers - Logs stack trace - Returns 500 Internal Server Error - Prevents server crash **Error Response:** ``` 500 Internal Server Error Internal server error ``` --- #### 2. Logger **Purpose:** Request logging for monitoring and debugging **File:** `internal/middleware/logger.go` **Logged Information:** - HTTP method - Request path - Response status code - Response time - Client IP **Log Format:** ``` 2025-12-06 10:30:15 | 200 | 15.234ms | 192.168.1.100 | GET /cv 2025-12-06 10:30:16 | 429 | 0.523ms | 192.168.1.101 | POST /api/contact ``` --- #### 3. SecurityHeaders **Purpose:** Comprehensive security headers **File:** `internal/middleware/security.go` **Headers Applied:** | Header | Value | Purpose | |--------|-------|---------| | X-Frame-Options | SAMEORIGIN | Prevent clickjacking | | X-Content-Type-Options | nosniff | Prevent MIME sniffing | | X-XSS-Protection | 1; mode=block | XSS protection (legacy) | | Referrer-Policy | strict-origin-when-cross-origin | Privacy | | Permissions-Policy | geolocation=(), camera=(), ... | Disable unnecessary features | | Content-Security-Policy | See CSP section below | XSS/injection prevention | | Strict-Transport-Security | max-age=31536000 (prod only) | Force HTTPS | **Content Security Policy (CSP):** ``` default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net https://esm.sh https://matomo.morenorub.io; 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 https://matomo.morenorub.io; frame-ancestors 'self'; base-uri 'self'; form-action 'self' ``` **HSTS (Production Only):** ``` Strict-Transport-Security: max-age=31536000; includeSubDomains; preload ``` --- #### 4. DynamicCacheControl **Purpose:** Appropriate caching for dynamic HTML pages **File:** `internal/middleware/security.go` **Cache Headers:** ```bash # Production Cache-Control: public, max-age=300, must-revalidate # 5 minutes # Development Cache-Control: no-cache, no-store, must-revalidate ``` **Benefits:** - Production: 5-minute cache reduces server load - Development: No cache for easy testing - `must-revalidate`: Ensures fresh content after expiry --- #### 5. PreferencesMiddleware **Purpose:** Parse and inject user preferences from cookies **File:** `internal/middleware/preferences.go` **Preferences:** - Language (en/es) - Theme (light/dark) - CV length (short/full) - Icons display (true/false) **Behavior:** 1. Read preferences cookies 2. Parse values 3. Inject into request context 4. Available to handlers via context **Cookie Names:** ``` cv_language=en cv_theme=dark cv_length=full cv_icons=true ``` ### Route-Specific Middleware #### BrowserOnly **Applied To:** `/api/contact` **Purpose:** Block non-browser HTTP clients **File:** `internal/middleware/browser_only.go` **Checks:** 1. **User-Agent Validation:** - Must be present - Must not be curl, wget, Postman, etc. 2. **Referer/Origin Validation:** - At least one must be present - Prevents direct API calls 3. **Custom Header Validation:** - `HX-Request: true` (HTMX), OR - `X-Requested-With: XMLHttpRequest`, OR - `X-Browser-Request: true` **Blocked User-Agents:** ```go curl, wget, postman, insomnia, httpie, python-requests, python-urllib, java, okhttp, go-http-client, axios, node-fetch, apache-httpclient, libwww-perl, php, ruby, scrapy, bot, crawler, spider ``` **Error Response (403 Forbidden):** ``` Forbidden: Browser access only ``` **Security Benefit:** Prevents automated bot submissions and API abuse --- #### OriginChecker **Applied To:** `/export/pdf` **Purpose:** Prevent external site hotlinking **File:** `internal/middleware/security.go` **Allowed Origins:** - `juan.andres.morenorub.io` (production domain) - `localhost` (development) - `127.0.0.1` (development) - Custom domains from `ALLOWED_ORIGINS` env var **Validation:** 1. Check `Origin` header (CORS requests) 2. Check `Referer` header (direct requests) 3. In production: Require at least referer for PDF endpoint **Error Response (403 Forbidden):** ``` Forbidden: External access not allowed Forbidden: Direct access not allowed (production only) ``` **Environment Configuration:** ```bash ALLOWED_ORIGINS="yourdomain.com,www.yourdomain.com" ``` --- #### RateLimiter **Applied To:** - `/api/contact` - 5 requests/hour - `/export/pdf` - 3 requests/minute **Purpose:** Prevent abuse and excessive resource usage **File:** `internal/middleware/security.go` **Implementation:** ```go // Contact form rate limiter contactRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour) // PDF export rate limiter pdfRateLimiter := middleware.NewRateLimiter(3, 1*time.Minute) ``` **Features:** - Per-IP tracking - Configurable limit and window - Automatic cleanup of expired entries - Thread-safe (sync.RWMutex) **Rate Limit Algorithm:** ``` 1. Get client IP (X-Forwarded-For → X-Real-IP → RemoteAddr) 2. Check if IP has entry 3. If no entry or expired: - Create new entry with count=1 - Set resetTime = now + window - Allow request 4. If entry exists and not expired: - If count >= limit: Deny - Else: Increment count, Allow ``` **Error Response (429 Too Many Requests):** ``` HTTP/1.1 429 Too Many Requests Retry-After: 60 Rate limit exceeded. Please try again later. ``` **HTMX Error Response:** ```html

Too Many Requests

You've submitted too many contact forms. Please wait an hour before trying again.

``` **Cleanup:** - Runs every 1 minute (general) or 10 minutes (contact) - Removes expired IP entries - Prevents memory leak --- #### CacheControl **Applied To:** `/static/*` **Purpose:** Aggressive caching for static assets **File:** `internal/middleware/security.go` **Cache Headers:** ```bash # Development Cache-Control: public, max-age=3600 # 1 hour # Production Cache-Control: public, max-age=86400 # 1 day ``` **Benefits:** - Reduces bandwidth - Improves page load speed - Offloads server processing **Cache Busting:** ```html ``` ## Security Features ### 1. HTTPS Enforcement (Production) ```go if os.Getenv("GO_ENV") == "production" { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") } ``` **Effect:** Forces HTTPS for 1 year, includes subdomains ### 2. Content Security Policy Prevents XSS and injection attacks by whitelisting allowed sources: ``` script-src 'self' https://unpkg.com https://cdn.jsdelivr.net style-src 'self' 'unsafe-inline' https://fonts.googleapis.com img-src 'self' data: https: ``` ### 3. Multi-Layer Bot Protection **Contact Form:** 1. BrowserOnly middleware 2. Honeypot field (`website` must be empty) 3. Timing validation (2s-24h) 4. Rate limiting (5/hour) **PDF Export:** 1. OriginChecker (prevents hotlinking) 2. Rate limiting (3/minute) ### 4. Email Header Injection Prevention ```go // Validation checks for newlines and email headers if ContainsEmailInjection(req.Subject) { return ValidationError{Field: "subject", Message: "Invalid characters"} } ``` **Blocked Patterns:** - `\r`, `\n` characters - `bcc:`, `cc:`, `to:`, `from:` - `content-type:`, `mime-version:` ### 5. XSS Prevention **Template Auto-Escaping:** ```html

{{.UserInput}}

``` **Validation Sanitization:** ```go Message string `validate:"required,trim,max=5000,sanitize"` // Result: HTML-escaped, newlines removed ``` ## Error Responses ### Standard Error Format ```json { "success": false, "error": "Error message", "errors": [ { "field": "email", "tag": "email", "message": "Invalid email address format" } ] } ``` ### HTTP Status Codes | Code | Meaning | Usage | |------|---------|-------| | 200 | OK | Successful request | | 302 | Found | PDF shortcut redirect | | 400 | Bad Request | Validation error | | 403 | Forbidden | BrowserOnly or OriginChecker failed | | 404 | Not Found | Route not found | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Server error, template error | | 503 | Service Unavailable | Health check failed | ## Configuration ### Environment Variables ```bash # Application environment GO_ENV=production # or "development" # Allowed origins for PDF export ALLOWED_ORIGINS="juan.andres.morenorub.io,www.juan.andres.morenorub.io" # Template hot reload (development) TEMPLATE_HOT_RELOAD=true # Server configuration PORT=8080 HOST=0.0.0.0 ``` ### Route Priority Routes are registered in order of specificity to avoid conflicts: ```go // 1. Specific patterns first mux.HandleFunc("/cv-jamr-", cvHandler.DefaultCVShortcut) mux.HandleFunc("/api/cmd-k", cvHandler.CmdKData) // 2. Protected endpoints mux.Handle("/api/contact", protectedContactHandler) mux.Handle("/export/pdf", protectedPDFHandler) // 3. General routes mux.HandleFunc("/", cvHandler.Home) mux.HandleFunc("/cv", cvHandler.CVContent) // 4. Static files (catch-all) mux.Handle("/static/", middleware.CacheControl(staticHandler)) ``` ## Performance Considerations ### 1. Middleware Order Optimization ```go // Fast-fail first (Recovery catches panics immediately) Recovery → Logger → SecurityHeaders → DynamicCache → Preferences → Mux ``` ### 2. Rate Limiter Efficiency ```go // RWMutex for concurrent reads type RateLimiter struct { mu sync.RWMutex // Read-heavy workload clients map[string]*rateLimitEntry } ``` **Performance:** - Allow check: ~100-200 ns - Memory per IP: ~48 bytes - Cleanup overhead: Negligible (1/min) ### 3. Template Caching Production mode (HotReload=false): - Templates loaded once at startup - Zero reload overhead - Thread-safe concurrent rendering ### 4. Static File Serving ```go // Native Go file server with proper cache headers http.FileServer(http.Dir("static")) ``` **Benefits:** - Efficient sendfile() syscall - Range request support - ETag generation - Gzip compression (if configured) ## Monitoring and Observability ### Request Logging ``` 2025-12-06 10:30:15 | 200 | 15.234ms | 192.168.1.100 | GET /cv 2025-12-06 10:30:16 | 429 | 0.523ms | 192.168.1.101 | POST /api/contact 2025-12-06 10:30:17 | 200 | 125.678ms| 192.168.1.102 | GET /export/pdf ``` **Logged Fields:** - Timestamp - Status code - Response time - Client IP - Method + Path ### Health Check Endpoint ```bash curl https://juan.andres.morenorub.io/health ``` **Response:** ```json { "status": "healthy", "timestamp": "2025-12-06T10:30:00Z", "version": "1.0.0" } ``` **Use Cases:** - Load balancer health checks - Uptime monitoring - Deployment verification ### Rate Limiter Statistics ```go // Available for monitoring dashboards func (rl *RateLimiter) GetStats() map[string]interface{} { return map[string]interface{}{ "total_clients": len(rl.clients), "limit": rl.limit, "window": rl.window.String(), } } ``` ## Testing Routes ### Manual Testing ```bash # Home page curl https://juan.andres.morenorub.io/ # Plain text CV curl https://juan.andres.morenorub.io/text # Health check curl https://juan.andres.morenorub.io/health # PDF export (will be blocked - needs browser) curl https://juan.andres.morenorub.io/export/pdf # Contact form (will be blocked - needs browser) curl -X POST https://juan.andres.morenorub.io/api/contact \ -H "Content-Type: application/json" \ -d '{"name":"Test","email":"test@example.com",...}' ``` ### Automated Testing ```go // Test route handlers func TestHomeHandler(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected 200, got %d", w.Code) } } // Test middleware func TestBrowserOnly(t *testing.T) { req := httptest.NewRequest("POST", "/api/contact", nil) req.Header.Set("User-Agent", "curl/7.68.0") w := httptest.NewRecorder() middleware.BrowserOnly(mockHandler).ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("Expected 403, got %d", w.Code) } } ``` ## Quick Reference ### Route Overview ``` Public: / → Home page /cv → CV content partial /text → Plain text CV /health → Health check HTMX: /switch-language → Toggle EN/ES /toggle/length → Toggle short/full /toggle/icons → Toggle icons /toggle/theme → Toggle light/dark API: /api/cmd-k → Command palette data /api/contact → Contact form (protected) Protected: /export/pdf → PDF generation (rate limited) /cv-jamr-* → PDF shortcuts Static: /static/* → CSS, JS, images, fonts ``` ### Middleware Chains ``` Global (all routes): Recovery → Logger → SecurityHeaders → DynamicCache → Preferences Contact Form: + BrowserOnly → RateLimiter(5/hour) PDF Export: + OriginChecker → RateLimiter(3/min) Static Files: + CacheControl ``` ### Security Headers ``` X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Referrer-Policy: strict-origin-when-cross-origin Content-Security-Policy: (comprehensive policy) Strict-Transport-Security: max-age=31536000 (production only) ``` ## Related Files - `internal/routes/routes.go` - Route setup and middleware chain - `internal/middleware/security.go` - Security middleware - `internal/middleware/browser_only.go` - BrowserOnly middleware - `internal/middleware/contact_rate_limit.go` - Contact rate limiting - `internal/middleware/logger.go` - Request logging - `internal/middleware/recovery.go` - Panic recovery - `internal/middleware/preferences.go` - User preferences - `internal/handlers/cv.go` - CV handlers - `internal/handlers/health.go` - Health check handler - `internal/handlers/cv_contact.go` - Contact form handler - `internal/handlers/cv_pdf.go` - PDF export handler ## See Also - [Validation System Documentation](go-validation-system.md) - [Template System Documentation](go-template-system.md) - [Go net/http Package](https://pkg.go.dev/net/http)