docs: Add comprehensive system architecture diagrams

Created detailed ASCII diagrams documenting the entire system architecture:

1. System Architecture (_go-learning/diagrams/01-system-architecture.md)
   - Overall system architecture with client/server/storage layers
   - Layered architecture (Presentation → Application → Business → Data)
   - Component interaction and HTTP request flow
   - Data flow from app start through per-request lifecycle
   - Package dependencies and file organization
This commit is contained in:
juanatsap
2025-11-20 20:17:29 +00:00
parent 9015cef098
commit faf3a2ca45
11 changed files with 4160 additions and 0 deletions
@@ -0,0 +1,272 @@
# System Architecture Diagram
## Overall System Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ CV Website System │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Client │────────▶│ Server │───────▶│ Storage │ │
│ │ Browser │◀────────│ (Bun/Go) │◀───────│ (Static) │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
│ │ │ │ │
│ │ HTMX │ Templates │ JSON │
│ │ HTTP │ Rendering │ Files │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ UI/UX │ │ Handlers │ │ Data Models │ │
│ │ Components │ │ Middleware │ │ CV/UI │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Layered Architecture
```
┌──────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ HTML Templates + HTMX + Hyperscript + CSS │ │
│ │ - Server-side rendering │ │
│ │ - Hypermedia-driven architecture │ │
│ │ - Progressive enhancement │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ HTTP Handlers (internal/handlers/) │ │
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
│ │ │ cv_pages.go │ cv_htmx.go │ cv_pdf.go │ │ │
│ │ │ Page render │ HTMX toggles │ PDF export │ │ │
│ │ └──────────────┴──────────────┴──────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Middleware Chain (internal/middleware/) │ │
│ │ Recovery → Logger → SecurityHeaders → Preferences │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Business Layer │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Data Models (internal/models/) │ │
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
│ │ │ cv/ │ ui/ │ Validation │ │ │
│ │ │ CV data │ UI strings │ Rules │ │ │
│ │ └──────────────┴──────────────┴──────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Services (internal/pdf/, internal/lang/) │ │
│ │ - PDF generation (chromedp) │ │
│ │ - Language handling │ │
│ │ - Template management │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Data Layer │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Static Files │ │
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
│ │ │ data/ │ templates/ │ static/ │ │ │
│ │ │ cv-*.json │ *.html │ css/js/ │ │ │
│ │ │ ui-*.json │ partials/ │ images/ │ │ │
│ │ └──────────────┴──────────────┴──────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
## Component Interaction
```
┌─────────────────────────────────────────────────────────────────┐
│ HTTP Request Flow │
└─────────────────────────────────────────────────────────────────┘
Client Request
├─→ Browser sends HTTP/HTMX request
┌─────────────┐
│ Router │ Match URL pattern
│ (ServeMux) │ ├─ / → Home
└─────────────┘ ├─ /cv → CVContent
│ ├─ /toggle/* → HTMX handlers
▼ └─ /export/pdf → ExportPDF
┌─────────────┐
│ Middleware │ Execute middleware chain
│ Chain │ ├─ Recovery (panic handler)
└─────────────┘ ├─ Logger (request logging)
│ ├─ SecurityHeaders (CSP, HSTS)
▼ └─ PreferencesMiddleware (cookies → context)
┌─────────────┐
│ Handler │ Process request
│ Function │ ├─ Parse request (typed)
└─────────────┘ ├─ Load data (models)
│ ├─ Prepare template data
▼ └─ Render response
┌─────────────┐
│ Template │ Server-side rendering
│ Rendering │ ├─ Load template
└─────────────┘ ├─ Execute with data
│ └─ Generate HTML
┌─────────────┐
│ Response │ Send to client
│ (HTML/PDF) │ └─ HTTP 200 OK
└─────────────┘
Client receives response
```
## Data Flow
```
┌────────────────────────────────────────────────────────────────┐
│ Data Flow Diagram │
└────────────────────────────────────────────────────────────────┘
Application Start
┌──────────────────────────────────────────┐
│ Load Configuration (config.Load()) │
│ ├─ Server settings (port, timeouts) │
│ └─ Template settings (dir, hot reload) │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Initialize Template Manager │
│ ├─ Scan template directory │
│ ├─ Parse all templates │
│ └─ Cache compiled templates │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Initialize Handlers │
│ └─ CVHandler with template manager │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Setup Routes + Middleware │
│ └─ routes.Setup(cvHandler, ...) │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Start HTTP Server │
│ └─ Listen on :8080 │
└──────────────────────────────────────────┘
Ready for Requests
─────────────────────────────────────────────────────────────────
Per-Request Flow
┌──────────────────────────────────────────┐
│ Request arrives │
│ └─ GET /?lang=es │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ PreferencesMiddleware reads cookies │
│ ├─ cv-length = "long" │
│ ├─ cv-icons = "show" │
│ ├─ cv-language = "es" │
│ └─ Store in request context │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Handler.Home() called │
│ ├─ Get preferences from context │
│ ├─ Validate language │
│ └─ Call prepareTemplateData("es") │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Load Data │
│ ├─ cvmodel.LoadCV("es") │
│ │ └─ Read data/cv-es.json │
│ └─ uimodel.LoadUI("es") │
│ └─ Read data/ui-es.json │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Process Data │
│ ├─ Calculate durations │
│ ├─ Split skills into columns │
│ └─ Add SEO metadata │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Render Template │
│ ├─ Get cached template │
│ ├─ Execute with data map │
│ └─ Generate HTML │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Send Response │
│ └─ HTTP 200 + HTML body │
└──────────────────────────────────────────┘
```
## Package Dependencies
```
main.go
├─→ internal/config
├─→ internal/templates
├─→ internal/handlers
│ ├─→ internal/middleware
│ ├─→ internal/models/cv
│ ├─→ internal/models/ui
│ ├─→ internal/pdf
│ └─→ internal/templates
├─→ internal/routes
│ ├─→ internal/handlers
│ └─→ internal/middleware
└─→ internal/middleware
internal/handlers/
├─ cv.go (constructor)
├─ cv_pages.go (renders)
├─ cv_htmx.go (toggles)
├─ cv_pdf.go (PDF export)
├─ cv_helpers.go (utilities)
├─ types.go (request/response)
└─ errors.go (error handling)
internal/middleware/
└─ preferences.go (cookie → context)
internal/models/
├─ cv/ (CV data structures)
└─ ui/ (UI text structures)
```
## Related Diagrams
- [Request Flow](./02-request-flow.md) - Detailed HTTP request lifecycle
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution order
- [Handler Organization](./04-handler-organization.md) - Handler file structure
+447
View File
@@ -0,0 +1,447 @@
# Request Flow Diagram
## Complete HTTP Request Lifecycle
```
┌─────────────────────────────────────────────────────────────────┐
│ Full Request Lifecycle │
└─────────────────────────────────────────────────────────────────┘
Client Browser
├─→ User visits /?lang=es&cv-length=long
┌─────────────────────────────────────────────────────────────┐
│ HTTP Request │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ GET /?lang=es&cv-length=long HTTP/1.1 │ │
│ │ Host: localhost:8080 │ │
│ │ Cookie: cv-length=short; cv-icons=show │ │
│ │ Accept: text/html │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Go HTTP Server (net/http) │
│ ├─ Port :8080 │
│ ├─ ServeMux Router │
│ └─ Match route pattern │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ MIDDLEWARE CHAIN (4 layers) │
│ │
│ 1. Recovery Middleware │
│ └─→ Wraps entire request in defer/recover │
│ │
│ 2. Logger Middleware │
│ └─→ Logs: [GET] / 127.0.0.1 │
│ │
│ 3. SecurityHeaders Middleware │
│ └─→ Sets: CSP, X-Frame-Options, etc. │
│ │
│ 4. PreferencesMiddleware │
│ ├─→ Reads cookies │
│ ├─→ Migrates old values │
│ └─→ Stores in request context │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ROUTER (ServeMux) │
│ ├─ Pattern: / │
│ ├─ Match: Home handler │
│ └─ Call: handler.Home(w, r) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ HANDLER: CVHandler.Home() │
│ (internal/handlers/cv_pages.go) │
│ │
│ Step 1: Get preferences from context │
│ ├─→ prefs := middleware.GetPreferences(r) │
│ └─→ Result: CVLength="long", CVLanguage="es" │
│ │
│ Step 2: Validate language from query params │
│ ├─→ lang := r.URL.Query().Get("lang") │
│ ├─→ Fallback to: prefs.CVLanguage if empty │
│ └─→ Validate: must be "en" or "es" │
│ │
│ Step 3: Prepare template data │
│ ├─→ Call: h.prepareTemplateData(lang) │
│ └─→ Returns: map with CV, UI, preferences │
│ │
│ Step 4: Render template │
│ ├─→ Call: h.tmpl.Render(w, "index.html", data) │
│ └─→ Returns: HTML response │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ TEMPLATE PREPARATION │
│ (prepareTemplateData helper) │
│ │
│ 1. Load CV data │
│ ├─→ cv, err := cvmodel.LoadCV(lang) │
│ └─→ Read: data/cv-es.json │
│ │
│ 2. Load UI strings │
│ ├─→ ui, err := uimodel.LoadUI(lang) │
│ └─→ Read: data/ui-es.json │
│ │
│ 3. Calculate experience durations │
│ └─→ For each experience: years/months │
│ │
│ 4. Split skills into columns │
│ └─→ Distribute skills evenly across columns │
│ │
│ 5. Build data map │
│ └─→ Return: CV, UI, preferences, SEO metadata │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ TEMPLATE RENDERING │
│ (internal/templates/manager.go) │
│ │
│ 1. Get cached template │
│ ├─→ tmpl := m.templates["index.html"] │
│ └─→ (or reload if hot reload enabled) │
│ │
│ 2. Execute template │
│ ├─→ tmpl.Execute(w, data) │
│ ├─→ Process: {{.CV.Name}}, {{range .CV.Experience}} │
│ └─→ Include partials: header, footer, sections │
│ │
│ 3. Generate HTML │
│ └─→ Full HTML page with data injected │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ RESPONSE GENERATION │
│ │
│ Headers: │
│ ├─ Content-Type: text/html; charset=utf-8 │
│ ├─ Content-Security-Policy: [CSP rules] │
│ ├─ X-Frame-Options: DENY │
│ └─ Set-Cookie: cv-language=es; Path=/; Max-Age=... │
│ │
│ Body: │
│ └─ <!DOCTYPE html> │
│ <html lang="es"> │
│ <head>...</head> │
│ <body> │
│ <!-- Full CV content --> │
│ </body> │
│ </html> │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ LOGGER MIDDLEWARE (completion) │
│ └─→ Log: Completed in 45ms (status: 200) │
└─────────────────────────────────────────────────────────────┘
Client Browser receives HTML
```
## HTMX Toggle Request Flow
```
┌─────────────────────────────────────────────────────────────┐
│ HTMX Toggle Request (Partial Update) │
└─────────────────────────────────────────────────────────────┘
User clicks toggle button
┌─────────────────────────────────────────────────────────────┐
│ HTMX Request │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ GET /toggle/length?current=short HTTP/1.1 │ │
│ │ HX-Request: true │ │
│ │ HX-Trigger: toggle-length-btn │ │
│ │ HX-Target: #main-content │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Middleware Chain (same as above)
┌─────────────────────────────────────────────────────────────┐
│ ROUTER │
│ ├─ Pattern: /toggle/length │
│ └─ Handler: CVHandler.ToggleCVLength(w, r) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ HANDLER: CVHandler.ToggleCVLength() │
│ (internal/handlers/cv_htmx.go) │
│ │
│ 1. Get current preferences │
│ └─→ prefs := middleware.GetPreferences(r) │
│ │
│ 2. Toggle state │
│ ├─→ currentLength := prefs.CVLength │
│ └─→ newLength := "long" if current == "short" │
│ │
│ 3. Save new preference │
│ └─→ middleware.SetPreferenceCookie(w, "cv-length", newLength) │
│ │
│ 4. Get language and prepare data │
│ └─→ data := h.prepareTemplateData(lang) │
│ │
│ 5. Render partial template │
│ └─→ h.tmpl.Render(w, "partials/cv_content.html", data) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PARTIAL TEMPLATE RENDERING │
│ └─ Only renders: partials/cv_content.html │
│ (Not full page, just the content section) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ HTMX Response │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ HTTP/1.1 200 OK │ │
│ │ Content-Type: text/html │ │
│ │ Set-Cookie: cv-length=long; Path=/; Max-Age=... │ │
│ │ │ │
│ │ <div id="main-content"> │ │
│ │ <!-- Updated CV content with long format --> │ │
│ │ </div> │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
HTMX swaps content in #main-content
(No page reload, instant update)
```
## PDF Export Request Flow
```
┌─────────────────────────────────────────────────────────────┐
│ PDF Export Request Flow │
└─────────────────────────────────────────────────────────────┘
User clicks "Export PDF"
┌─────────────────────────────────────────────────────────────┐
│ HTTP POST Request │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ POST /export/pdf HTTP/1.1 │ │
│ │ Content-Type: application/json │ │
│ │ Origin: http://localhost:8080 │ │
│ │ │ │
│ │ { │ │
│ │ "lang": "es", │ │
│ │ "length": "long", │ │
│ │ "icons": "show", │ │
│ │ "version": "with_skills" │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Global Middleware Chain
┌─────────────────────────────────────────────────────────────┐
│ ROUTE-SPECIFIC MIDDLEWARE │
│ │
│ 1. OriginChecker │
│ └─→ Verify same-origin request │
│ │
│ 2. RateLimiter │
│ └─→ Check: 3 requests/min per IP │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ HANDLER: CVHandler.ExportPDF() │
│ (internal/handlers/cv_pdf.go) │
│ │
│ 1. Parse and validate request │
│ ├─→ var req PDFExportRequest │
│ ├─→ json.NewDecoder(r.Body).Decode(&req) │
│ └─→ Validate: lang, length, icons, version │
│ │
│ 2. Render HTML for PDF │
│ ├─→ Build data map with preferences │
│ ├─→ Render to buffer: index.html template │
│ └─→ Result: Full HTML page in memory │
│ │
│ 3. Generate PDF │
│ ├─→ Call: pdf.GeneratePDF(htmlContent, pdfOptions) │
│ └─→ Uses: chromedp to render HTML → PDF │
│ │
│ 4. Send PDF response │
│ ├─→ Set headers: application/pdf │
│ ├─→ Set filename: CV-[Name]-[lang].pdf │
│ └─→ Write: PDF bytes to response │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PDF GENERATION (chromedp) │
│ (internal/pdf/generator.go) │
│ │
│ 1. Launch headless Chrome │
│ └─→ chromedp.NewContext() │
│ │
│ 2. Navigate to data URL │
│ └─→ Load HTML content │
│ │
│ 3. Wait for rendering │
│ └─→ Ensure fonts, images loaded │
│ │
│ 4. Generate PDF │
│ ├─→ chromedp.PrintToPDF() with options │
│ ├─→ A4 size, margins, print background │
│ └─→ Return: PDF bytes │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PDF Response │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ HTTP/1.1 200 OK │ │
│ │ Content-Type: application/pdf │ │
│ │ Content-Disposition: attachment; filename="CV-..." │ │
│ │ Content-Length: 245678 │ │
│ │ │ │
│ │ [PDF binary data] │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Browser triggers download
```
## Error Handling Flow
```
┌─────────────────────────────────────────────────────────────┐
│ Error Handling Flow │
└─────────────────────────────────────────────────────────────┘
Request with invalid language
Handler validation detects error
├─→ Create: InvalidLanguageError("xx")
┌─────────────────────────────────────────────────────────────┐
│ DomainError Created │
│ ├─ Code: INVALID_LANGUAGE │
│ ├─ Message: "Unsupported language: xx (use 'en' or 'es')" │
│ ├─ StatusCode: 400 │
│ └─ Field: "lang" │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Handler.HandleError(w, r, err) │
│ (internal/handlers/errors.go) │
│ │
│ 1. Check if DomainError │
│ └─→ Extract: code, message, status, field │
│ │
│ 2. Log error │
│ └─→ log.Printf("[ERROR] %s: %s", code, message) │
│ │
│ 3. Build error response │
│ ├─→ Create: ErrorInfo struct │
│ └─→ Create: APIResponse wrapper │
│ │
│ 4. Send error response │
│ ├─→ Set status: 400 Bad Request │
│ ├─→ Set content-type: application/json │
│ └─→ Write: JSON error response │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Error Response │
│ { │
│ "success": false, │
│ "error": { │
│ "code": "INVALID_LANGUAGE", │
│ "message": "Unsupported language: xx", │
│ "field": "lang" │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
Client receives error
```
## Performance Metrics
```
Typical Request Timings:
┌─────────────────────────────────────────────────────┐
│ Component Time % │
├─────────────────────────────────────────────────────┤
│ Middleware overhead ~350 μs 0.7% │
│ ├─ Recovery ~10 ns │
│ ├─ Logger ~100 μs │
│ ├─ SecurityHeaders ~50 ns │
│ └─ Preferences ~200 μs │
│ │
│ Handler processing ~500 μs 1.0% │
│ ├─ Get preferences ~10 μs │
│ ├─ Validate input ~50 μs │
│ └─ Prepare data ~440 μs │
│ │
│ Data loading ~2 ms 4.0% │
│ ├─ Load CV JSON ~1 ms │
│ └─ Load UI JSON ~1 ms │
│ │
│ Template rendering ~45 ms 90% │
│ ├─ Template execution ~40 ms │
│ └─ HTML generation ~5 ms │
│ │
│ Response transmission ~2 ms 4.0% │
├─────────────────────────────────────────────────────┤
│ TOTAL REQUEST TIME ~50 ms 100% │
└─────────────────────────────────────────────────────┘
PDF Export Timings:
┌─────────────────────────────────────────────────────┐
│ Component Time % │
├─────────────────────────────────────────────────────┤
│ Middleware + Handler ~1 ms 0.1% │
│ Template rendering ~50 ms 5% │
│ Chrome launch/navigation ~200 ms 20% │
│ PDF generation ~700 ms 70% │
│ Response transmission ~50 ms 5% │
├─────────────────────────────────────────────────────┤
│ TOTAL PDF EXPORT TIME ~1 sec 100% │
└─────────────────────────────────────────────────────┘
```
## Related Diagrams
- [System Architecture](./01-system-architecture.md) - Overall system design
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution details
- [Handler Organization](./04-handler-organization.md) - Handler structure
- [Error Handling Flow](./06-error-handling-flow.md) - Error propagation details
@@ -0,0 +1,315 @@
# Middleware Chain Diagram
## Middleware Execution Order
```
HTTP Request
┌────────────────────────────────────────────────────────────┐
│ MIDDLEWARE CHAIN │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. Recovery Middleware │ │
│ │ - Catches panics │ │
│ │ - Logs stack trace │ │
│ │ - Returns 500 error │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 2. Logger Middleware │ │
│ │ - Logs request method, path, IP │ │
│ │ - Measures request duration │ │
│ │ - Logs response status │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 3. SecurityHeaders Middleware │ │
│ │ - Sets CSP header │ │
│ │ - Sets X-Frame-Options │ │
│ │ - Sets X-Content-Type-Options │ │
│ │ - Sets Referrer-Policy │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 4. PreferencesMiddleware │ │
│ │ - Reads preference cookies │ │
│ │ - Migrates old values │ │
│ │ - Stores in request context │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────┼────────────────────────────────┘
┌───────────────┐
│ Router │
│ (ServeMux) │
└───────────────┘
┌───────────────┐
│ Handler │
└───────────────┘
```
## Detailed Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ Request Processing Flow │
└─────────────────────────────────────────────────────────────────┘
Client Request: GET /?lang=es
╔═══════════════════════════════════════════════════════════╗
║ RECOVERY MIDDLEWARE ║
╠═══════════════════════════════════════════════════════════╣
║ defer func() { ║
║ if err := recover(); err != nil { ║
║ log error + stack trace ║
║ http.Error(w, "Internal Server Error", 500) ║
║ } ║
║ }() ║
║ ║
║ next.ServeHTTP(w, r) ────────────────┐ ║
╚════════════════════════════════════════│══════════════════╝
╔═══════════════════════════════════════════════════════════╗
║ LOGGER MIDDLEWARE ║
╠═══════════════════════════════════════════════════════════╣
║ start := time.Now() ║
║ log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr) ║
║ ║
║ wrapped := responseWriter wrapper ║
║ next.ServeHTTP(wrapped, r) ──────────┐ ║
║ │ ║
║ duration := time.Since(start) │ ║
║ log.Printf("Completed in %v (status: %d)", duration, status) ║
╚═════════════════════════════════════════│════════════════╝
╔═══════════════════════════════════════════════════════════╗
║ SECURITY HEADERS MIDDLEWARE ║
╠═══════════════════════════════════════════════════════════╣
║ w.Header().Set("Content-Security-Policy", CSP_POLICY) ║
║ w.Header().Set("X-Frame-Options", "DENY") ║
║ w.Header().Set("X-Content-Type-Options", "nosniff") ║
║ w.Header().Set("Referrer-Policy", "strict-origin") ║
║ ║
║ next.ServeHTTP(w, r) ────────────────┐ ║
╚════════════════════════════════════════│══════════════════╝
╔═══════════════════════════════════════════════════════════╗
║ PREFERENCES MIDDLEWARE ║
╠═══════════════════════════════════════════════════════════╣
║ // Read cookies ║
║ prefs := &Preferences{ ║
║ CVLength: getCookie(r, "cv-length", "short"), ║
║ CVIcons: getCookie(r, "cv-icons", "show"), ║
║ CVLanguage: getCookie(r, "cv-language", "en"), ║
║ CVTheme: getCookie(r, "cv-theme", "default"), ║
║ ColorTheme: getCookie(r, "color-theme", "light"), ║
║ } ║
║ ║
║ // Migrate old values ║
║ if prefs.CVLength == "extended" { ║
║ prefs.CVLength = "long" ║
║ } ║
║ ║
║ // Store in context ║
║ ctx := context.WithValue(r.Context(), PreferencesKey, prefs) ║
║ next.ServeHTTP(w, r.WithContext(ctx)) ───┐ ║
╚═════════════════════════════════════════════│════════════╝
┌──────────────────┐
│ ROUTER HANDLER │
│ │
│ Matches route │
│ Calls handler │
└──────────────────┘
┌──────────────────┐
│ HANDLER FUNC │
│ │
│ Processes req │
│ Returns resp │
└──────────────────┘
```
## Route-Specific Middleware
```
┌────────────────────────────────────────────────────────────────┐
│ Route-Specific Middleware Example │
│ (PDF Export Endpoint) │
└────────────────────────────────────────────────────────────────┘
Global Middleware Chain (all routes)
├─ Recovery
├─ Logger
├─ SecurityHeaders
└─ PreferencesMiddleware
┌─────────────────────────────────────────┐
│ Router (ServeMux) │
│ │
│ /export/pdf → pdfHandler (protected) │
│ │ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ Route-Specific Chain │ │
│ │ │ │
│ │ 1. OriginChecker │ │
│ │ └─ Verify same origin│ │
│ │ │ │
│ │ 2. RateLimiter │ │
│ │ └─ 3 req/min per IP │ │
│ │ │ │
│ │ 3. ExportPDF Handler │ │
│ │ └─ Generate PDF │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────┘
```
## Middleware Wrapping Pattern
```go
// Middleware function signature
type Middleware func(http.Handler) http.Handler
// Wrapping example
handler := routes.Setup(cvHandler, healthHandler)
// Returns:
// Recovery(
// Logger(
// SecurityHeaders(
// PreferencesMiddleware(mux)
// )
// )
// )
// Execution flow (unwraps from outside to inside):
Request
enters Recovery
enters Logger
enters SecurityHeaders
enters PreferencesMiddleware
enters mux/handler
handler processes
exits PreferencesMiddleware
exits SecurityHeaders
exits Logger (logs duration)
exits Recovery
Response
```
## Context Flow
```
┌─────────────────────────────────────────────────────────────┐
│ Context Values Through Middleware │
└─────────────────────────────────────────────────────────────┘
Initial Request Context
├─ Empty context.Background()
PreferencesMiddleware
├─ Reads cookies
├─ Creates Preferences struct
└─ Adds to context
└─→ ctx = context.WithValue(r.Context(), PreferencesKey, prefs)
┌──────────────────────────────────────┐
│ Modified Request Context │
│ │
│ PreferencesKey → &Preferences{ │
│ CVLength: "long", │
│ CVIcons: "show", │
│ CVLanguage: "es", │
│ CVTheme: "default", │
│ ColorTheme: "light", │
│ } │
└──────────────────────────────────────┘
Handler receives request with enriched context
├─→ prefs := middleware.GetPreferences(r)
│ // Retrieves from context
└─→ lang := middleware.GetLanguage(r)
// Helper that calls GetPreferences
```
## Error Handling in Middleware
```
┌────────────────────────────────────────────────────────────┐
│ Error Handling Flow │
└────────────────────────────────────────────────────────────┘
Recovery Middleware
│ Normal Flow:
│ ┌─────────────────────────────────────┐
│ │ next.ServeHTTP(w, r) │
│ │ ↓ │
│ │ Handler processes successfully │
│ │ ↓ │
│ │ Returns response │
│ └─────────────────────────────────────┘
│ Panic Flow:
│ ┌─────────────────────────────────────┐
│ │ next.ServeHTTP(w, r) │
│ │ ↓ │
│ │ Handler panics! │
│ │ ↓ │
│ │ defer recover() catches it │
│ │ ↓ │
│ │ log.Printf("PANIC: %v\\n%s", │
│ │ err, debug.Stack()) │
│ │ ↓ │
│ │ http.Error(w, "Internal Error", 500)│
│ └─────────────────────────────────────┘
Response to client
```
## Performance Characteristics
```
Middleware Performance Impact (per request):
Recovery: ~10 ns (defer overhead)
Logger: ~100 μs (time measurements, string formatting)
SecurityHeaders: ~50 ns (header setting)
Preferences: ~200 μs (cookie parsing, context creation)
Total overhead: ~350 μs per request
Handler time: ~1-5 ms (template rendering)
Total request: ~1.5-5.5 ms
```
## Related Diagrams
- [System Architecture](./01-system-architecture.md) - Overall system design
- [Request Flow](./02-request-flow.md) - Complete HTTP request lifecycle
- [Error Handling](./06-error-handling-flow.md) - Error propagation
@@ -0,0 +1,389 @@
# Handler Organization Diagram
## Handler File Structure
```
internal/handlers/
├── cv.go Constructor, shared state
├── cv_pages.go Full page renders (Home, CVContent)
├── cv_htmx.go HTMX partial updates (4 toggles)
├── cv_pdf.go PDF export endpoint
├── cv_helpers.go Shared utilities (prepareTemplateData, etc.)
├── types.go Request/response types, validation
├── errors.go Error handling, domain errors
├── cv_pages_test.go Tests for page handlers
├── cv_htmx_test.go Tests for HTMX handlers
└── benchmarks_test.go Benchmark tests
```
## File Responsibilities
```
┌──────────────────────────────────────────────────────────────┐
│ cv.go │
│ (Constructor & State) │
├──────────────────────────────────────────────────────────────┤
│ type CVHandler struct { │
│ tmpl *templates.Manager // Template renderer │
│ host string // For absolute URLs │
│ } │
│ │
│ func NewCVHandler(tmpl, host) *CVHandler │
│ └─→ Constructor for handler initialization │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ cv_pages.go │
│ (Full Page Renders) │
├──────────────────────────────────────────────────────────────┤
│ func (h *CVHandler) Home(w, r) │
│ └─→ GET / │
│ ├─ Get preferences from context │
│ ├─ Validate language parameter │
│ ├─ Prepare full template data │
│ └─ Render: index.html (full page) │
│ │
│ func (h *CVHandler) CVContent(w, r) │
│ └─→ GET /cv │
│ ├─ Get preferences from context │
│ ├─ Prepare template data │
│ └─ Render: partials/cv_content.html │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ cv_htmx.go │
│ (HTMX Partial Updates) │
├──────────────────────────────────────────────────────────────┤
│ func (h *CVHandler) ToggleCVLength(w, r) │
│ └─→ GET /toggle/length?current=short │
│ ├─ Get current preferences │
│ ├─ Toggle: short ↔ long │
│ ├─ Save cookie: cv-length │
│ └─ Render: partials/cv_content.html │
│ │
│ func (h *CVHandler) ToggleCVIcons(w, r) │
│ └─→ GET /toggle/icons?current=show │
│ ├─ Toggle: show ↔ hide │
│ ├─ Save cookie: cv-icons │
│ └─ Render: partials/cv_content.html │
│ │
│ func (h *CVHandler) ToggleCVTheme(w, r) │
│ └─→ GET /toggle/theme?current=default │
│ ├─ Toggle: default ↔ minimal │
│ ├─ Save cookie: cv-theme │
│ └─ Render: partials/cv_content.html │
│ │
│ func (h *CVHandler) ToggleLanguage(w, r) │
│ └─→ GET /toggle/language?current=en │
│ ├─ Toggle: en ↔ es │
│ ├─ Save cookie: cv-language │
│ └─ Render: index.html (full page for i18n) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ cv_pdf.go │
│ (PDF Export) │
├──────────────────────────────────────────────────────────────┤
│ func (h *CVHandler) ExportPDF(w, r) │
│ └─→ POST /export/pdf │
│ ├─ Parse JSON request body │
│ ├─ Validate: lang, length, icons, version │
│ ├─ Render HTML to buffer │
│ ├─ Generate PDF via chromedp │
│ └─ Send PDF response with download header │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ cv_helpers.go │
│ (Shared Utilities) │
├──────────────────────────────────────────────────────────────┤
│ func (h *CVHandler) prepareTemplateData(lang) map │
│ └─→ Shared data preparation for all handlers │
│ ├─ Load CV data: cvmodel.LoadCV(lang) │
│ ├─ Load UI strings: uimodel.LoadUI(lang) │
│ ├─ Calculate durations for experiences │
│ ├─ Split skills into columns │
│ ├─ Add SEO metadata │
│ └─ Return: complete data map │
│ │
│ func (h *CVHandler) getFullURL(path) string │
│ └─→ Build absolute URLs for SEO/PDF │
│ └─ Return: http://host/path │
│ │
│ func validateLanguage(lang) error │
│ └─→ Validate language parameter │
│ └─ Check: lang in ["en", "es"] │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ types.go │
│ (Request/Response Types) │
├──────────────────────────────────────────────────────────────┤
│ // Request Types │
│ type PDFExportRequest struct { │
│ Lang string `json:"lang" validate:"required,oneof=en es"` │
│ Length string `json:"length" validate:"required,oneof=short long"` │
│ Icons string `json:"icons" validate:"required,oneof=show hide"` │
│ Version string `json:"version" validate:"required,oneof=with_skills clean"` │
│ } │
│ │
│ // Response Types │
│ type APIResponse struct { │
│ Success bool `json:"success"` │
│ Data interface{} `json:"data,omitempty"` │
│ Error *ErrorInfo `json:"error,omitempty"` │
│ Meta *MetaInfo `json:"meta,omitempty"` │
│ } │
│ │
│ type ErrorInfo struct { │
│ Code string `json:"code"` │
│ Message string `json:"message"` │
│ Field string `json:"field,omitempty"` │
│ } │
│ │
│ type MetaInfo struct { │
│ Timestamp time.Time `json:"timestamp"` │
│ RequestID string `json:"request_id,omitempty"` │
│ } │
│ │
│ // Constructor Functions │
│ func NewAPIResponse(data interface{}) *APIResponse │
│ func NewErrorResponse(code, message string) *APIResponse │
│ func NewPDFExportRequest() *PDFExportRequest │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ errors.go │
│ (Error Handling) │
├──────────────────────────────────────────────────────────────┤
│ // Error Codes │
│ type ErrorCode string │
│ const ( │
│ ErrCodeInvalidLanguage = "INVALID_LANGUAGE" │
│ ErrCodeInvalidLength = "INVALID_LENGTH" │
│ ErrCodeInvalidIcons = "INVALID_ICONS" │
│ ErrCodePDFGeneration = "PDF_GENERATION" │
│ ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED" │
│ // ... 8 more error codes │
│ ) │
│ │
│ // Domain Error Type │
│ type DomainError struct { │
│ Code ErrorCode │
│ Message string │
│ Err error │
│ StatusCode int │
│ Field string │
│ } │
│ │
│ // Error Constructors │
│ func InvalidLanguageError(lang) *DomainError │
│ func InvalidLengthError(length) *DomainError │
│ func PDFGenerationError(err) *DomainError │
│ // ... 10 more constructors │
│ │
│ // Error Handler │
│ func (h *CVHandler) HandleError(w, r, err) │
│ └─→ Centralized error handling │
│ ├─ Log error with code │
│ ├─ Build error response │
│ └─ Send JSON error │
└──────────────────────────────────────────────────────────────┘
```
## Handler Dependencies
```
┌────────────────────────────────────────────────────────────┐
│ Handler Dependencies │
└────────────────────────────────────────────────────────────┘
CVHandler
├─→ internal/templates (template rendering)
│ └─→ Manager.Render(w, name, data)
├─→ internal/models/cv (CV data)
│ └─→ LoadCV(lang) (*CV, error)
├─→ internal/models/ui (UI strings)
│ └─→ LoadUI(lang) (*UI, error)
├─→ internal/middleware (preferences)
│ ├─→ GetPreferences(r) *Preferences
│ ├─→ GetLanguage(r) string
│ ├─→ IsLongCV(r) bool
│ └─→ SetPreferenceCookie(w, name, value)
├─→ internal/pdf (PDF generation)
│ └─→ GeneratePDF(html, options) ([]byte, error)
└─→ encoding/json (JSON parsing)
└─→ json.NewDecoder(r.Body).Decode(&req)
```
## Handler Call Flow
```
┌────────────────────────────────────────────────────────────┐
│ Typical Handler Call Flow │
└────────────────────────────────────────────────────────────┘
Request arrives
┌─────────────────────┐
│ Middleware Chain │
│ (preferences set) │
└─────────────────────┘
┌─────────────────────┐
│ Handler Method │
│ (cv_pages.go) │
└─────────────────────┘
├─→ middleware.GetPreferences(r)
│ └─→ Extract from request context
├─→ validateLanguage(lang)
│ └─→ Check valid language
├─→ h.prepareTemplateData(lang)
│ │ (cv_helpers.go)
│ │
│ ├─→ cvmodel.LoadCV(lang)
│ │ └─→ Read data/cv-{lang}.json
│ │
│ ├─→ uimodel.LoadUI(lang)
│ │ └─→ Read data/ui-{lang}.json
│ │
│ ├─→ calculateDurations()
│ │ └─→ For each experience
│ │
│ └─→ splitSkillsIntoColumns()
│ └─→ Distribute evenly
└─→ h.tmpl.Render(w, "index.html", data)
└─→ Execute template with data
```
## Handler Testing Structure
```
┌────────────────────────────────────────────────────────────┐
│ Handler Tests │
└────────────────────────────────────────────────────────────┘
cv_pages_test.go
├─ TestHome
│ ├─ Valid requests (en, es)
│ ├─ Invalid language
│ ├─ With preferences
│ └─ Default fallback
└─ TestCVContent
├─ Valid language
├─ With preferences
└─ Error handling
cv_htmx_test.go
├─ TestToggleCVLength
│ ├─ short → long
│ ├─ long → short
│ └─ Cookie setting
├─ TestToggleCVIcons
│ ├─ show → hide
│ └─ hide → show
├─ TestToggleCVTheme
│ └─ default ↔ minimal
└─ TestToggleLanguage
└─ en ↔ es
benchmarks_test.go
├─ BenchmarkHome
├─ BenchmarkCVContent
├─ BenchmarkToggleCVLength
├─ BenchmarkToggleCVIcons
├─ BenchmarkToggleCVTheme
├─ BenchmarkToggleLanguage
├─ BenchmarkExportPDF
├─ BenchmarkPrepareTemplateData
├─ BenchmarkValidateLanguage
├─ BenchmarkErrorResponse
└─ BenchmarkNewAPIResponse
```
## Handler Pattern Summary
```
┌────────────────────────────────────────────────────────────┐
│ Handler Organization Principles │
└────────────────────────────────────────────────────────────┘
1. SEPARATION BY RESPONSIBILITY
├─ Pages: Full page renders
├─ HTMX: Partial updates
├─ PDF: Export functionality
└─ Helpers: Shared utilities
2. TYPE SAFETY
├─ Structured request types
├─ Structured response types
└─ Validation tags
3. ERROR HANDLING
├─ Domain-specific errors
├─ Error codes
└─ Centralized error handler
4. TESTABILITY
├─ Unit tests per file
├─ Integration tests
└─ Benchmark tests
5. DEPENDENCY INJECTION
├─ Template manager injected
├─ No global state
└─ Easy to mock
6. MIDDLEWARE INTEGRATION
├─ Preferences from context
├─ Helper functions
└─ Clean separation
```
## Performance Profile
```
Handler Performance Characteristics:
┌─────────────────────────────────────────────────────────┐
│ Handler Time Allocations │
├─────────────────────────────────────────────────────────┤
│ Home() ~50 ms ~1200 allocs │
│ CVContent() ~45 ms ~1100 allocs │
│ ToggleCVLength() ~45 ms ~1100 allocs │
│ ToggleCVIcons() ~45 ms ~1100 allocs │
│ ToggleCVTheme() ~45 ms ~1100 allocs │
│ ToggleLanguage() ~50 ms ~1200 allocs │
│ ExportPDF() ~1000 ms ~5000 allocs │
├─────────────────────────────────────────────────────────┤
│ prepareTemplateData() ~2 ms ~50 allocs │
│ validateLanguage() ~10 ns 0 allocs │
└─────────────────────────────────────────────────────────┘
Memory Profile:
- Most allocations in template rendering (~90%)
- JSON parsing minimal (<1%)
- Helper functions optimized (zero-alloc where possible)
```
## Related Diagrams
- [System Architecture](./01-system-architecture.md) - Overall system design
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution
- [Error Handling Flow](./06-error-handling-flow.md) - Error propagation
+481
View File
@@ -0,0 +1,481 @@
# Data Models Diagram
## Data Model Overview
```
┌──────────────────────────────────────────────────────────────┐
│ Data Model Structure │
└──────────────────────────────────────────────────────────────┘
internal/models/
├── cv/ CV data structures
│ ├── cv.go Main CV model
│ ├── personal.go Personal information
│ ├── experience.go Work experience
│ ├── education.go Education history
│ ├── skills.go Technical skills
│ └── languages.go Language proficiency
└── ui/ UI text structures
├── ui.go Main UI model
├── sections.go Section titles
├── buttons.go Button labels
└── messages.go User messages
```
## CV Data Model
```
┌──────────────────────────────────────────────────────────────┐
│ CV Structure (cv/cv.go) │
├──────────────────────────────────────────────────────────────┤
│ type CV struct { │
│ Personal Personal `json:"personal"` │
│ Summary string `json:"summary"` │
│ Experience []Experience `json:"experience"` │
│ Education []Education `json:"education"` │
│ Skills Skills `json:"skills"` │
│ Languages []Language `json:"languages"` │
│ } │
│ │
│ Methods: │
│ ├─ LoadCV(lang string) (*CV, error) │
│ │ └─→ Read data/cv-{lang}.json │
│ │ │
│ ├─ Validate() error │
│ │ └─→ Ensure all required fields present │
│ │ │
│ └─ CalculateDurations() │
│ └─→ Calculate years/months for experiences │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Personal Information (cv/personal.go) │
├──────────────────────────────────────────────────────────────┤
│ type Personal struct { │
│ Name string `json:"name"` │
│ Title string `json:"title"` │
│ Email string `json:"email"` │
│ Phone string `json:"phone,omitempty"` │
│ Location string `json:"location"` │
│ Website string `json:"website,omitempty"` │
│ LinkedIn string `json:"linkedin,omitempty"` │
│ GitHub string `json:"github,omitempty"` │
│ Photo string `json:"photo,omitempty"` │
│ } │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Work Experience (cv/experience.go) │
├──────────────────────────────────────────────────────────────┤
│ type Experience struct { │
│ Company string `json:"company"` │
│ Position string `json:"position"` │
│ Location string `json:"location"` │
│ StartDate string `json:"start_date"` │
│ EndDate string `json:"end_date,omitempty"` │
│ Current bool `json:"current"` │
│ Description string `json:"description"` │
│ Highlights []string `json:"highlights"` │
│ Duration string `json:"-"` // Calculated │
│ } │
│ │
│ Methods: │
│ ├─ CalculateDuration() string │
│ │ ├─ Parse StartDate and EndDate │
│ │ ├─ Calculate difference │
│ │ └─ Return: "2 years 3 months" or "Present" │
│ │ │
│ └─ IsCurrent() bool │
│ └─→ Check if EndDate is empty or Current flag │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Education (cv/education.go) │
├──────────────────────────────────────────────────────────────┤
│ type Education struct { │
│ Institution string `json:"institution"` │
│ Degree string `json:"degree"` │
│ Field string `json:"field"` │
│ Location string `json:"location"` │
│ StartDate string `json:"start_date"` │
│ EndDate string `json:"end_date,omitempty"` │
│ GPA string `json:"gpa,omitempty"` │
│ Honors []string `json:"honors,omitempty"` │
│ } │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Skills (cv/skills.go) │
├──────────────────────────────────────────────────────────────┤
│ type Skills struct { │
│ Technical []Skill `json:"technical"` │
│ Soft []Skill `json:"soft"` │
│ Tools []Skill `json:"tools"` │
│ } │
│ │
│ type Skill struct { │
│ Name string `json:"name"` │
│ Level string `json:"level,omitempty"` │
│ Icon string `json:"icon,omitempty"` │
│ Category string `json:"category,omitempty"` │
│ } │
│ │
│ Methods: │
│ └─ SplitIntoColumns(numCols int) [][]Skill │
│ └─→ Distribute skills evenly across columns │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Languages (cv/languages.go) │
├──────────────────────────────────────────────────────────────┤
│ type Language struct { │
│ Name string `json:"name"` │
│ Level string `json:"level"` │
│ Proficiency string `json:"proficiency,omitempty"` │
│ } │
│ │
│ Levels: Native, Fluent, Professional, Intermediate, Basic │
└──────────────────────────────────────────────────────────────┘
```
## UI Data Model
```
┌──────────────────────────────────────────────────────────────┐
│ UI Structure (ui/ui.go) │
├──────────────────────────────────────────────────────────────┤
│ type UI struct { │
│ Sections Sections `json:"sections"` │
│ Buttons Buttons `json:"buttons"` │
│ Messages Messages `json:"messages"` │
│ Labels Labels `json:"labels"` │
│ } │
│ │
│ Methods: │
│ └─ LoadUI(lang string) (*UI, error) │
│ └─→ Read data/ui-{lang}.json │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Section Titles (ui/sections.go) │
├──────────────────────────────────────────────────────────────┤
│ type Sections struct { │
│ Summary string `json:"summary"` │
│ Experience string `json:"experience"` │
│ Education string `json:"education"` │
│ Skills string `json:"skills"` │
│ Languages string `json:"languages"` │
│ Contact string `json:"contact"` │
│ } │
│ │
│ Example (English): │
│ { │
│ "summary": "Professional Summary", │
│ "experience": "Work Experience", │
│ "education": "Education", │
│ "skills": "Technical Skills", │
│ "languages": "Languages", │
│ "contact": "Contact Information" │
│ } │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Button Labels (ui/buttons.go) │
├──────────────────────────────────────────────────────────────┤
│ type Buttons struct { │
│ ExportPDF string `json:"export_pdf"` │
│ ToggleLength string `json:"toggle_length"` │
│ ToggleIcons string `json:"toggle_icons"` │
│ ToggleTheme string `json:"toggle_theme"` │
│ ToggleLanguage string `json:"toggle_language"` │
│ Print string `json:"print"` │
│ } │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ User Messages (ui/messages.go) │
├──────────────────────────────────────────────────────────────┤
│ type Messages struct { │
│ Loading string `json:"loading"` │
│ Error string `json:"error"` │
│ Success string `json:"success"` │
│ PDFGenerating string `json:"pdf_generating"` │
│ PDFReady string `json:"pdf_ready"` │
│ } │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Labels (ui/labels.go) │
├──────────────────────────────────────────────────────────────┤
│ type Labels struct { │
│ ShortCV string `json:"short_cv"` │
│ LongCV string `json:"long_cv"` │
│ ShowIcons string `json:"show_icons"` │
│ HideIcons string `json:"hide_icons"` │
│ Light string `json:"light"` │
│ Dark string `json:"dark"` │
│ } │
└──────────────────────────────────────────────────────────────┘
```
## Data Flow
```
┌────────────────────────────────────────────────────────────┐
│ Data Flow │
└────────────────────────────────────────────────────────────┘
JSON Files (data/)
├── cv-en.json English CV data
├── cv-es.json Spanish CV data
├── ui-en.json English UI strings
└── ui-es.json Spanish UI strings
┌─────────────────────────┐
│ LoadCV(lang) │
│ LoadUI(lang) │
│ (internal/models/) │
└─────────────────────────┘
├─→ Parse JSON
├─→ Validate structure
└─→ Return typed structs
┌─────────────────────────┐
│ Handler │
│ (internal/handlers/) │
└─────────────────────────┐
├─→ Calculate durations
├─→ Split skills
└─→ Build template data map
┌─────────────────────────┐
│ Template Rendering │
│ (templates/) │
└─────────────────────────┘
HTML Response
```
## Example Data Structure
```
┌────────────────────────────────────────────────────────────┐
│ Sample CV Data (data/cv-en.json) │
└────────────────────────────────────────────────────────────┘
{
"personal": {
"name": "John Doe",
"title": "Senior Software Engineer",
"email": "john@example.com",
"location": "San Francisco, CA",
"linkedin": "linkedin.com/in/johndoe",
"github": "github.com/johndoe"
},
"summary": "Experienced software engineer with 8+ years...",
"experience": [
{
"company": "Tech Corp",
"position": "Senior Software Engineer",
"location": "San Francisco, CA",
"start_date": "2020-01",
"end_date": "",
"current": true,
"description": "Leading backend development...",
"highlights": [
"Designed and implemented microservices architecture",
"Reduced API response time by 60%",
"Mentored 5 junior developers"
]
}
],
"education": [
{
"institution": "University of California",
"degree": "Bachelor of Science",
"field": "Computer Science",
"location": "Berkeley, CA",
"start_date": "2012-09",
"end_date": "2016-05",
"gpa": "3.8/4.0"
}
],
"skills": {
"technical": [
{"name": "Go", "level": "Expert", "icon": "golang"},
{"name": "JavaScript", "level": "Advanced", "icon": "js"},
{"name": "Python", "level": "Intermediate", "icon": "python"}
],
"tools": [
{"name": "Docker", "icon": "docker"},
{"name": "Kubernetes", "icon": "k8s"},
{"name": "Git", "icon": "git"}
]
},
"languages": [
{"name": "English", "level": "Native"},
{"name": "Spanish", "level": "Fluent"}
]
}
┌────────────────────────────────────────────────────────────┐
│ Sample UI Data (data/ui-en.json) │
└────────────────────────────────────────────────────────────┘
{
"sections": {
"summary": "Professional Summary",
"experience": "Work Experience",
"education": "Education",
"skills": "Technical Skills",
"languages": "Languages"
},
"buttons": {
"export_pdf": "Export PDF",
"toggle_length": "Toggle Length",
"toggle_icons": "Toggle Icons",
"toggle_theme": "Toggle Theme",
"toggle_language": "Switch Language"
},
"messages": {
"loading": "Loading...",
"error": "An error occurred",
"pdf_generating": "Generating PDF...",
"pdf_ready": "PDF is ready for download"
},
"labels": {
"short_cv": "Short",
"long_cv": "Long",
"show_icons": "Show Icons",
"hide_icons": "Hide Icons"
}
}
```
## Data Validation
```
┌────────────────────────────────────────────────────────────┐
│ Validation Rules │
└────────────────────────────────────────────────────────────┘
CV Validation:
├─ Personal
│ ├─ Name: Required, non-empty
│ ├─ Title: Required, non-empty
│ ├─ Email: Required, valid email format
│ └─ Location: Required, non-empty
├─ Experience
│ ├─ Company: Required, non-empty
│ ├─ Position: Required, non-empty
│ ├─ StartDate: Required, valid date (YYYY-MM)
│ └─ EndDate: Optional, must be after StartDate if present
├─ Education
│ ├─ Institution: Required, non-empty
│ ├─ Degree: Required, non-empty
│ └─ Field: Required, non-empty
├─ Skills
│ ├─ Name: Required, non-empty
│ └─ Level: Optional, one of [Basic, Intermediate, Advanced, Expert]
└─ Languages
├─ Name: Required, non-empty
└─ Level: Required, one of [Native, Fluent, Professional, Intermediate, Basic]
UI Validation:
├─ All section titles: Required, non-empty
├─ All button labels: Required, non-empty
└─ All messages: Required, non-empty
```
## Model Lifecycle
```
┌────────────────────────────────────────────────────────────┐
│ Model Lifecycle │
└────────────────────────────────────────────────────────────┘
Application Start
└─→ Models NOT loaded (lazy loading)
Request Arrives (lang=es)
├─→ Handler calls LoadCV("es")
│ ├─ Check cache (if caching enabled)
│ ├─ Read data/cv-es.json
│ ├─ Parse JSON → CV struct
│ ├─ Validate struct
│ └─ Return *CV
├─→ Handler calls LoadUI("es")
│ ├─ Read data/ui-es.json
│ ├─ Parse JSON → UI struct
│ └─ Return *UI
└─→ Handler processes data
├─ Calculate durations
├─ Split skills
└─ Render template
Next Request (lang=es)
└─→ Models reloaded (no persistent cache)
(Each request loads fresh data for hot reload)
```
## Data Transformation
```
┌────────────────────────────────────────────────────────────┐
│ Data Transformation Pipeline │
└────────────────────────────────────────────────────────────┘
JSON (static)
├─ "start_date": "2020-01"
└─ "end_date": ""
Go Struct (typed)
├─ StartDate: "2020-01"
├─ EndDate: ""
└─ Duration: "" (empty)
Calculate Duration
├─ Parse dates
├─ Calculate difference
└─ Format: "3 years 2 months"
Template Data (enriched)
├─ StartDate: "2020-01"
├─ EndDate: "Present"
└─ Duration: "3 years 2 months"
HTML (rendered)
└─ <span class="duration">3 years 2 months</span>
```
## Related Diagrams
- [System Architecture](./01-system-architecture.md) - Overall system design
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
- [Template Rendering](./07-template-rendering.md) - Template processing
@@ -0,0 +1,492 @@
# Error Handling Flow Diagram
## Error Handling Architecture
```
┌──────────────────────────────────────────────────────────────┐
│ Error Handling Architecture │
└──────────────────────────────────────────────────────────────┘
Error Types:
├── Domain Errors Application-level business logic errors
├── Validation Errors Input validation failures
├── System Errors Infrastructure/system failures
└── Panic Recovery Runtime panic handling
```
## Domain Error Structure
```
┌──────────────────────────────────────────────────────────────┐
│ DomainError (internal/handlers/errors.go) │
├──────────────────────────────────────────────────────────────┤
│ type DomainError struct { │
│ Code ErrorCode // Enum error code │
│ Message string // Human-readable message │
│ Err error // Underlying error (if any) │
│ StatusCode int // HTTP status code │
│ Field string // Field that caused error │
│ } │
│ │
│ func (e *DomainError) Error() string │
│ func (e *DomainError) Unwrap() error │
│ func (e *DomainError) WithField(field string) *DomainError │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Error Codes │
├──────────────────────────────────────────────────────────────┤
│ type ErrorCode string │
│ │
│ const ( │
│ // Input Validation (400) │
│ ErrCodeInvalidLanguage = "INVALID_LANGUAGE" │
│ ErrCodeInvalidLength = "INVALID_LENGTH" │
│ ErrCodeInvalidIcons = "INVALID_ICONS" │
│ ErrCodeInvalidTheme = "INVALID_THEME" │
│ ErrCodeInvalidVersion = "INVALID_VERSION" │
│ ErrCodeValidationFailed = "VALIDATION_FAILED" │
│ │
│ // Resource Errors (404, 500) │
│ ErrCodeDataNotFound = "DATA_NOT_FOUND" │
│ ErrCodeTemplateNotFound = "TEMPLATE_NOT_FOUND" │
│ ErrCodeTemplateError = "TEMPLATE_ERROR" │
│ │
│ // Processing Errors (500) │
│ ErrCodePDFGeneration = "PDF_GENERATION" │
│ ErrCodeInternalError = "INTERNAL_ERROR" │
│ │
│ // Rate Limiting (429) │
│ ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED" │
│ │
│ // Security (403) │
│ ErrCodeOriginMismatch = "ORIGIN_MISMATCH" │
│ ) │
└──────────────────────────────────────────────────────────────┘
```
## Error Flow Patterns
### Pattern 1: Validation Error
```
┌────────────────────────────────────────────────────────────┐
│ Validation Error Flow │
└────────────────────────────────────────────────────────────┘
Request: GET /?lang=xx
┌─────────────────────────┐
│ Handler.Home() │
│ (cv_pages.go) │
└─────────────────────────┘
├─→ lang := r.URL.Query().Get("lang")
│ // lang = "xx"
├─→ err := validateLanguage(lang)
│ // "xx" not in ["en", "es"]
┌─────────────────────────────────────────────────────────────┐
│ validateLanguage(lang) │
│ (cv_helpers.go) │
│ │
│ if lang != "en" && lang != "es" { │
│ return InvalidLanguageError(lang) │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ InvalidLanguageError(lang) │
│ (errors.go) │
│ │
│ return NewDomainError( │
│ ErrCodeInvalidLanguage, │
│ fmt.Sprintf("Unsupported language: %s", lang), │
│ http.StatusBadRequest, │
│ ).WithField("lang") │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Handler receives error │
│ │
│ if err != nil { │
│ h.HandleError(w, r, err) │
│ return │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ HandleError(w, r, err) │
│ (errors.go) │
│ │
│ 1. Cast to DomainError │
│ domErr, ok := err.(*DomainError) │
│ │
│ 2. Log error │
│ log.Printf("[ERROR] %s: %s", domErr.Code, domErr.Message) │
│ │
│ 3. Build response │
│ response := NewErrorResponse( │
│ string(domErr.Code), │
│ domErr.Message, │
│ ) │
│ response.Error.Field = domErr.Field │
│ │
│ 4. Send JSON error │
│ w.Header().Set("Content-Type", "application/json") │
│ w.WriteHeader(domErr.StatusCode) │
│ json.NewEncoder(w).Encode(response) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Client Response │
│ │
│ HTTP/1.1 400 Bad Request │
│ Content-Type: application/json │
│ │
│ { │
│ "success": false, │
│ "error": { │
│ "code": "INVALID_LANGUAGE", │
│ "message": "Unsupported language: xx (use 'en' or 'es')", │
│ "field": "lang" │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
```
### Pattern 2: Data Loading Error
```
┌────────────────────────────────────────────────────────────┐
│ Data Loading Error Flow │
└────────────────────────────────────────────────────────────┘
Handler calls LoadCV("es")
┌─────────────────────────────────────────────────────────────┐
│ cvmodel.LoadCV(lang) │
│ (internal/models/cv/cv.go) │
│ │
│ 1. Build file path │
│ filePath := fmt.Sprintf("data/cv-%s.json", lang) │
│ │
│ 2. Read file │
│ data, err := os.ReadFile(filePath) │
│ if err != nil { │
│ return nil, fmt.Errorf("failed to read CV: %w", err) │
│ } │
│ │
│ 3. Parse JSON │
│ var cv CV │
│ err = json.Unmarshal(data, &cv) │
│ if err != nil { │
│ return nil, fmt.Errorf("failed to parse CV: %w", err) │
│ } │
│ │
│ 4. Validate │
│ if err := cv.Validate(); err != nil { │
│ return nil, fmt.Errorf("invalid CV data: %w", err) │
│ } │
│ │
│ 5. Return │
│ return &cv, nil │
└─────────────────────────────────────────────────────────────┘
│ Error Case: File not found
┌─────────────────────────────────────────────────────────────┐
│ Handler receives error │
│ │
│ cv, err := cvmodel.LoadCV(lang) │
│ if err != nil { │
│ // Wrap in DomainError │
│ domErr := DataNotFoundError("CV", lang) │
│ domErr.Err = err │
│ h.HandleError(w, r, domErr) │
│ return │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Client Response │
│ │
│ HTTP/1.1 500 Internal Server Error │
│ Content-Type: application/json │
│ │
│ { │
│ "success": false, │
│ "error": { │
│ "code": "DATA_NOT_FOUND", │
│ "message": "CV data not found for language: es" │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
```
### Pattern 3: PDF Generation Error
```
┌────────────────────────────────────────────────────────────┐
│ PDF Generation Error Flow │
└────────────────────────────────────────────────────────────┘
Handler calls GeneratePDF()
┌─────────────────────────────────────────────────────────────┐
│ pdf.GeneratePDF(htmlContent, options) │
│ (internal/pdf/generator.go) │
│ │
│ 1. Create context │
│ ctx, cancel := chromedp.NewContext(...) │
│ defer cancel() │
│ │
│ 2. Launch Chrome │
│ if err := chromedp.Run(ctx, ...); err != nil { │
│ return nil, fmt.Errorf("chrome launch: %w", err) │
│ } │
│ │
│ 3. Navigate and render │
│ err := chromedp.Run(ctx, │
│ chromedp.Navigate(dataURL), │
│ chromedp.WaitReady("body"), │
│ chromedp.PrintToPDF(&pdfBytes), │
│ ) │
│ if err != nil { │
│ return nil, fmt.Errorf("PDF generation: %w", err) │
│ } │
│ │
│ 4. Return PDF bytes │
│ return pdfBytes, nil │
└─────────────────────────────────────────────────────────────┘
│ Error Case: Chrome failed
┌─────────────────────────────────────────────────────────────┐
│ Handler.ExportPDF receives error │
│ (cv_pdf.go) │
│ │
│ pdfBytes, err := pdf.GeneratePDF(htmlContent, opts) │
│ if err != nil { │
│ domErr := PDFGenerationError(err) │
│ h.HandleError(w, r, domErr) │
│ return │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Client Response │
│ │
│ HTTP/1.1 500 Internal Server Error │
│ Content-Type: application/json │
│ │
│ { │
│ "success": false, │
│ "error": { │
│ "code": "PDF_GENERATION", │
│ "message": "Failed to generate PDF. Please try again." │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
```
### Pattern 4: Panic Recovery
```
┌────────────────────────────────────────────────────────────┐
│ Panic Recovery Flow │
└────────────────────────────────────────────────────────────┘
Request enters system
┌─────────────────────────────────────────────────────────────┐
│ Recovery Middleware │
│ (internal/middleware/recovery.go) │
│ │
│ func Recovery(next http.Handler) http.Handler { │
│ return http.HandlerFunc(func(w, r) { │
│ defer func() { │
│ if err := recover(); err != nil { │
│ // Capture panic │
│ stack := debug.Stack() │
│ │
│ // Log with stack trace │
│ log.Printf("PANIC: %v\n%s", err, stack) │
│ │
│ // Send error response │
│ http.Error(w, │
│ "Internal Server Error", │
│ http.StatusInternalServerError) │
│ } │
│ }() │
│ │
│ // Continue to next handler │
│ next.ServeHTTP(w, r) │
│ }) │
│ } │
└─────────────────────────────────────────────────────────────┘
│ Normal flow: no panic
├──────────────────────────────────┐
▼ │ Panic occurs
Handler executes ▼
│ ┌─────────────────────────────────┐
│ │ panic("something went wrong") │
│ └─────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────┐
│ │ defer recover() catches it │
│ │ ├─ Get stack trace │
│ │ ├─ Log error + stack │
│ │ └─ Send 500 response │
│ └─────────────────────────────────┘
▼ │
Response sent ▼
┌─────────────────────────────────┐
│ Client receives 500 │
└─────────────────────────────────┘
```
## Error Response Formats
```
┌────────────────────────────────────────────────────────────┐
│ Error Response Formats │
└────────────────────────────────────────────────────────────┘
Standard API Error (JSON):
{
"success": false,
"error": {
"code": "INVALID_LANGUAGE",
"message": "Unsupported language: xx (use 'en' or 'es')",
"field": "lang"
}
}
Validation Error with Multiple Fields:
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"fields": {
"lang": "Invalid language",
"length": "Invalid length"
}
}
}
Internal Error (Generic):
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again."
}
}
HTML Error Page (for page requests):
<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body>
<h1>Oops! Something went wrong</h1>
<p>We're sorry, but we couldn't process your request.</p>
<p>Error: INVALID_LANGUAGE</p>
<a href="/">Go back home</a>
</body>
</html>
```
## Error Logging
```
┌────────────────────────────────────────────────────────────┐
│ Error Logging │
└────────────────────────────────────────────────────────────┘
Log Format:
[ERROR] <ERROR_CODE>: <message>
[ERROR] Additional context: <details>
[ERROR] Stack trace (if panic):
<stack trace lines>
Examples:
[ERROR] INVALID_LANGUAGE: Unsupported language: xx (use 'en' or 'es')
[ERROR] Field: lang
[ERROR] PDF_GENERATION: Failed to generate PDF
[ERROR] Underlying error: chrome launch failed: context deadline exceeded
[ERROR] PANIC: runtime error: invalid memory address
[ERROR] Stack trace:
goroutine 23 [running]:
main.(*CVHandler).Home(...)
/app/internal/handlers/cv_pages.go:42
...
```
## Error Handling Best Practices
```
┌────────────────────────────────────────────────────────────┐
│ Error Handling Best Practices │
└────────────────────────────────────────────────────────────┘
1. USE TYPED ERRORS
✓ return InvalidLanguageError(lang)
✗ return errors.New("invalid language")
2. WRAP ERRORS WITH CONTEXT
✓ return fmt.Errorf("failed to load CV: %w", err)
✗ return err
3. LOG BEFORE RESPONDING
✓ log.Printf("[ERROR] %s", err)
h.HandleError(w, r, err)
✗ h.HandleError(w, r, err) // No logging
4. USE APPROPRIATE STATUS CODES
✓ 400 for validation errors
404 for not found
429 for rate limiting
500 for server errors
✗ Always returning 500
5. DON'T LEAK INTERNAL DETAILS
✓ "Failed to generate PDF. Please try again."
✗ "chromedp: chrome crashed at line 42 in generator.go"
6. PROVIDE ACTIONABLE MESSAGES
✓ "Unsupported language: xx (use 'en' or 'es')"
✗ "Invalid input"
7. USE RECOVERY MIDDLEWARE
✓ Catch all panics at middleware level
✗ Let panics crash the server
8. INCLUDE FIELD INFORMATION
✓ error.WithField("lang")
✗ Generic error without field context
```
## Related Diagrams
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution
- [Handler Organization](./04-handler-organization.md) - Handler structure
@@ -0,0 +1,541 @@
# Template Rendering Diagram
## Template System Architecture
```
┌──────────────────────────────────────────────────────────────┐
│ Template System Architecture │
└──────────────────────────────────────────────────────────────┘
internal/templates/
├── manager.go Template manager (caching, rendering)
└── functions.go Custom template functions
templates/
├── index.html Main page template
├── partials/ Reusable components
│ ├── header.html
│ ├── footer.html
│ ├── cv_content.html
│ ├── experience.html
│ ├── education.html
│ ├── skills.html
│ └── languages.html
└── layouts/ Layout templates
└── base.html
```
## Template Manager
```
┌──────────────────────────────────────────────────────────────┐
│ Template Manager (internal/templates/manager.go) │
├──────────────────────────────────────────────────────────────┤
│ type Manager struct { │
│ templates map[string]*template.Template │
│ config *config.TemplateConfig │
│ mu sync.RWMutex // Thread-safe access │
│ } │
│ │
│ type TemplateConfig struct { │
│ Dir string // templates/ │
│ PartialsDir string // templates/partials/ │
│ HotReload bool // Reload on every render │
│ } │
│ │
│ Methods: │
│ ├─ NewManager(config) (*Manager, error) │
│ │ └─→ Initialize and load all templates │
│ │ │
│ ├─ Render(w, name, data) error │
│ │ └─→ Execute template with data │
│ │ │
│ ├─ loadTemplates() error │
│ │ └─→ Parse and cache all templates │
│ │ │
│ └─ reloadIfNeeded() error │
│ └─→ Reload templates if hot reload enabled │
└──────────────────────────────────────────────────────────────┘
```
## Template Loading Flow
```
┌────────────────────────────────────────────────────────────┐
│ Template Loading Flow │
└────────────────────────────────────────────────────────────┘
Application Start
┌─────────────────────────────────────────────────────────────┐
│ NewManager(config) │
│ (internal/templates/manager.go) │
│ │
│ 1. Create manager │
│ m := &Manager{ │
│ templates: make(map[string]*template.Template), │
│ config: config, │
│ } │
│ │
│ 2. Load all templates │
│ if err := m.loadTemplates(); err != nil { │
│ return nil, err │
│ } │
│ │
│ 3. Return manager │
│ return m, nil │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ loadTemplates() │
│ │
│ 1. Scan template directory │
│ files, err := filepath.Glob(config.Dir + "/*.html") │
│ │
│ 2. For each template file: │
│ ├─ Create new template │
│ │ tmpl := template.New(name) │
│ │ │
│ ├─ Add custom functions │
│ │ tmpl.Funcs(customFunctions()) │
│ │ │
│ ├─ Parse main template │
│ │ tmpl.ParseFiles(file) │
│ │ │
│ ├─ Parse partials │
│ │ tmpl.ParseGlob(config.PartialsDir + "/*.html") │
│ │ │
│ └─ Cache template │
│ m.templates[name] = tmpl │
│ │
│ 3. Log loaded templates │
│ log.Printf("Loaded %d templates", len(m.templates)) │
└─────────────────────────────────────────────────────────────┘
```
## Template Rendering Flow
```
┌────────────────────────────────────────────────────────────┐
│ Template Rendering Flow │
└────────────────────────────────────────────────────────────┘
Handler calls Render()
┌─────────────────────────────────────────────────────────────┐
│ Manager.Render(w, "index.html", data) │
│ (internal/templates/manager.go) │
│ │
│ 1. Lock for reading │
│ m.mu.RLock() │
│ defer m.mu.RUnlock() │
│ │
│ 2. Hot reload check │
│ if m.config.HotReload { │
│ m.mu.RUnlock() │
│ m.mu.Lock() │
│ m.loadTemplates() // Reload all templates │
│ m.mu.Unlock() │
│ m.mu.RLock() │
│ } │
│ │
│ 3. Get template from cache │
│ tmpl, ok := m.templates[name] │
│ if !ok { │
│ return fmt.Errorf("template not found: %s", name) │
│ } │
│ │
│ 4. Execute template │
│ err := tmpl.Execute(w, data) │
│ if err != nil { │
│ return fmt.Errorf("template execution: %w", err) │
│ } │
│ │
│ 5. Return │
│ return nil │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Template Execution │
│ │
│ 1. Parse template directives │
│ {{.CV.Personal.Name}} │
│ {{range .CV.Experience}}...{{end}} │
│ {{template "partials/header.html" .}} │
│ │
│ 2. Execute custom functions │
│ {{formatDate .StartDate}} │
│ {{join .Highlights ", "}} │
│ {{lower .CVLanguage}} │
│ │
│ 3. Include partials │
│ {{template "partials/cv_content.html" .}} │
│ {{template "partials/experience.html" .}} │
│ │
│ 4. Generate HTML │
│ Write to http.ResponseWriter │
└─────────────────────────────────────────────────────────────┘
```
## Template Hierarchy
```
┌────────────────────────────────────────────────────────────┐
│ Template Hierarchy │
└────────────────────────────────────────────────────────────┘
index.html (Main Template)
├─→ {{template "partials/header.html" .}}
│ └─→ Navigation, language toggle, theme toggle
├─→ {{template "partials/cv_content.html" .}}
│ │
│ ├─→ {{template "partials/experience.html" .}}
│ │ └─→ {{range .CV.Experience}}
│ │ ├─ Company, position, dates
│ │ ├─ {{.Duration}} (calculated)
│ │ └─ {{range .Highlights}}
│ │
│ ├─→ {{template "partials/education.html" .}}
│ │ └─→ {{range .CV.Education}}
│ │ ├─ Institution, degree, field
│ │ └─ Dates, GPA, honors
│ │
│ ├─→ {{template "partials/skills.html" .}}
│ │ └─→ {{range .SkillsColumns}}
│ │ └─ {{range .}}
│ │ ├─ Skill name
│ │ ├─ Level badge
│ │ └─ Icon (if enabled)
│ │
│ └─→ {{template "partials/languages.html" .}}
│ └─→ {{range .CV.Languages}}
│ ├─ Language name
│ └─ Proficiency level
└─→ {{template "partials/footer.html" .}}
└─→ PDF export button, copyright
```
## Template Data Structure
```
┌────────────────────────────────────────────────────────────┐
│ Template Data Structure │
└────────────────────────────────────────────────────────────┘
Data passed to templates:
map[string]interface{}{
// CV Data
"CV": &cvmodel.CV{
Personal: cvmodel.Personal{
Name: "John Doe",
Title: "Senior Software Engineer",
Email: "john@example.com",
Location: "San Francisco, CA",
},
Experience: []cvmodel.Experience{
{
Company: "Tech Corp",
Position: "Senior Engineer",
StartDate: "2020-01",
EndDate: "",
Current: true,
Duration: "3 years 2 months", // Calculated
Highlights: []string{...},
},
},
Education: []cvmodel.Education{...},
Skills: cvmodel.Skills{...},
Languages: []cvmodel.Language{...},
},
// UI Strings
"UI": &uimodel.UI{
Sections: uimodel.Sections{
Summary: "Professional Summary",
Experience: "Work Experience",
Education: "Education",
Skills: "Technical Skills",
Languages: "Languages",
},
Buttons: uimodel.Buttons{...},
Messages: uimodel.Messages{...},
},
// User Preferences
"Preferences": &middleware.Preferences{
CVLength: "long",
CVIcons: "show",
CVLanguage: "es",
CVTheme: "default",
ColorTheme: "light",
},
// Processed Data
"SkillsColumns": [][]cvmodel.Skill{
[]cvmodel.Skill{...}, // Column 1
[]cvmodel.Skill{...}, // Column 2
[]cvmodel.Skill{...}, // Column 3
},
// SEO Metadata
"PageTitle": "John Doe - Senior Software Engineer",
"MetaDescription": "Professional CV of John Doe...",
"CanonicalURL": "http://localhost:8080/",
"OGImage": "http://localhost:8080/static/images/og-image.png",
}
```
## Custom Template Functions
```
┌────────────────────────────────────────────────────────────┐
│ Custom Template Functions │
│ (internal/templates/functions.go) │
└────────────────────────────────────────────────────────────┘
template.FuncMap{
// String manipulation
"lower": strings.ToLower,
"upper": strings.ToUpper,
"title": strings.Title,
// Date formatting
"formatDate": func(date string) string {
if date == "" {
return "Present"
}
t, _ := time.Parse("2006-01", date)
return t.Format("Jan 2006")
},
// Array operations
"join": strings.Join,
"split": strings.Split,
// Math
"add": func(a, b int) int {
return a + b
},
"multiply": func(a, b int) int {
return a * b
},
// Conditional helpers
"eq": func(a, b interface{}) bool {
return a == b
},
"ne": func(a, b interface{}) bool {
return a != b
},
// HTML safety
"safe": func(s string) template.HTML {
return template.HTML(s)
},
}
Usage in templates:
{{formatDate .StartDate}}
// "2020-01" → "Jan 2020"
{{join .Highlights ", "}}
// ["foo", "bar"] → "foo, bar"
{{if eq .CVLength "long"}}
<!-- Show long content -->
{{end}}
{{.Description | safe}}
// Render HTML without escaping
```
## Template Conditionals
```
┌────────────────────────────────────────────────────────────┐
│ Template Conditionals │
└────────────────────────────────────────────────────────────┘
Show/Hide based on CV length:
{{if eq .Preferences.CVLength "long"}}
<!-- Show full details -->
<div class="experience-highlights">
{{range .Highlights}}
<li>{{.}}</li>
{{end}}
</div>
{{end}}
Show/Hide based on icons preference:
{{if eq .Preferences.CVIcons "show"}}
<i class="icon-{{.Icon}}"></i>
{{end}}
Conditional classes:
<div class="cv-section {{if eq .Preferences.CVTheme "minimal"}}minimal{{end}}">
...
</div>
Language-specific content:
{{if eq .Preferences.CVLanguage "es"}}
<span>Experiencia Profesional</span>
{{else}}
<span>Professional Experience</span>
{{end}}
Current vs. past experience:
{{if .Current}}
<span class="badge current">Present</span>
{{else}}
<span>{{formatDate .EndDate}}</span>
{{end}}
```
## Template Performance
```
┌────────────────────────────────────────────────────────────┐
│ Template Performance │
└────────────────────────────────────────────────────────────┘
Performance Characteristics:
┌─────────────────────────────────────────────────────────┐
│ Operation Time Notes │
├─────────────────────────────────────────────────────────┤
│ Template Loading ~50ms On app start │
│ ├─ Parse templates ~40ms Compile Go templates│
│ └─ Cache templates ~10ms Store in map │
│ │
│ Template Rendering ~45ms Per request │
│ ├─ Template lookup ~10ns Map access │
│ ├─ Template execute ~40ms Main cost │
│ ├─ Partial includes ~5ms Include partials │
│ └─ Function calls ~100μs Custom functions │
│ │
│ Hot Reload ~50ms If enabled │
│ └─ Reload all ~50ms Parse again │
└─────────────────────────────────────────────────────────┘
Optimization Strategies:
1. Template Caching
└─→ Pre-compile templates at startup
Serve from memory cache
2. Hot Reload (Development Only)
└─→ Reload on every request for dev
Disable in production for speed
3. Minimize Partials
└─→ Balance reusability vs. overhead
Each partial adds ~1ms
4. Pre-calculate Data
└─→ Calculate durations in handler
Split skills before rendering
5. Use Buffer Pool
└─→ Reuse buffers for rendering
Reduce allocations
```
## Template Error Handling
```
┌────────────────────────────────────────────────────────────┐
│ Template Error Handling │
└────────────────────────────────────────────────────────────┘
Error Types:
1. Template Not Found
Error: template "foo.html" not found
Cause: Template doesn't exist in cache
Fix: Create template file, reload
2. Parse Error
Error: template: index.html:42: unexpected "}"
Cause: Syntax error in template
Fix: Check template syntax
3. Execution Error
Error: template: executing "index.html": map has no entry for key "Foo"
Cause: Missing data in template data map
Fix: Ensure all required data passed
4. Function Error
Error: template: function "unknownFunc" not defined
Cause: Custom function not registered
Fix: Register function in FuncMap
Error Flow:
Template Error
├─→ Logged with stack trace
│ log.Printf("[ERROR] Template: %v", err)
├─→ Wrapped in DomainError
│ TemplateError(err)
└─→ Sent as 500 response
{
"success": false,
"error": {
"code": "TEMPLATE_ERROR",
"message": "Failed to render page"
}
}
```
## Hot Reload Flow
```
┌────────────────────────────────────────────────────────────┐
│ Hot Reload Flow │
│ (Development Mode) │
└────────────────────────────────────────────────────────────┘
Developer edits template
Next request arrives
┌─────────────────────────────────────────────────────────────┐
│ Render() called │
│ │
│ if m.config.HotReload { │
│ // Reload all templates │
│ m.mu.Lock() │
│ m.loadTemplates() │
│ m.mu.Unlock() │
│ } │
│ │
│ // Use fresh templates │
│ tmpl := m.templates[name] │
│ tmpl.Execute(w, data) │
└─────────────────────────────────────────────────────────────┘
Page rendered with updated template
(No server restart needed)
⚠️ Hot reload disabled in production for performance
```
## Related Diagrams
- [System Architecture](./01-system-architecture.md) - Overall system design
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
- [Handler Organization](./04-handler-organization.md) - Handler structure
- [Data Models](./05-data-models.md) - Data structures
+529
View File
@@ -0,0 +1,529 @@
# PDF Generation Diagram
## PDF Export Architecture
```
┌──────────────────────────────────────────────────────────────┐
│ PDF Export Architecture │
└──────────────────────────────────────────────────────────────┘
Client (Browser)
├─→ User clicks "Export PDF"
┌─────────────────────────┐
│ Modal with options │
│ ├─ Language (en/es) │
│ ├─ Length (short/long) │
│ ├─ Icons (show/hide) │
│ └─ Version (with/clean)│
└─────────────────────────┘
POST /export/pdf
┌─────────────────────────┐
│ Route Middleware │
│ ├─ OriginChecker │
│ └─ RateLimiter │
└─────────────────────────┘
┌─────────────────────────┐
│ CVHandler.ExportPDF() │
│ (cv_pdf.go) │
└─────────────────────────┘
┌─────────────────────────┐
│ PDF Generator │
│ (internal/pdf/) │
└─────────────────────────┘
┌─────────────────────────┐
│ Chromedp │
│ (Headless Chrome) │
└─────────────────────────┘
PDF Response
```
## PDF Generation Flow
```
┌────────────────────────────────────────────────────────────┐
│ PDF Generation Flow │
└────────────────────────────────────────────────────────────┘
1. REQUEST VALIDATION
┌─────────────────────────────────────────────────────────┐
│ Handler.ExportPDF(w, r) │
│ (internal/handlers/cv_pdf.go) │
│ │
│ // Parse JSON request │
│ var req PDFExportRequest │
│ err := json.NewDecoder(r.Body).Decode(&req) │
│ │
│ // Validate fields │
│ if req.Lang != "en" && req.Lang != "es" { │
│ return InvalidLanguageError(req.Lang) │
│ } │
│ if req.Length != "short" && req.Length != "long" { │
│ return InvalidLengthError(req.Length) │
│ } │
└─────────────────────────────────────────────────────────┘
2. HTML GENERATION
┌─────────────────────────────────────────────────────────┐
│ // Build template data │
│ data := map[string]interface{}{ │
│ "CV": cv, │
│ "UI": ui, │
│ "Preferences": &middleware.Preferences{ │
│ CVLength: req.Length, │
│ CVIcons: req.Icons, │
│ CVLanguage: req.Lang, │
│ }, │
│ "SkillsColumns": skillColumns, │
│ "IsPDF": true, // PDF-specific flag │
│ } │
│ │
│ // Render to buffer │
│ var buf bytes.Buffer │
│ err := h.tmpl.Render(&buf, "index.html", data) │
│ htmlContent := buf.String() │
└─────────────────────────────────────────────────────────┘
3. PDF OPTIONS
┌─────────────────────────────────────────────────────────┐
│ opts := pdf.Options{ │
│ PaperSize: pdf.A4, │
│ Orientation: pdf.Portrait, │
│ MarginTop: "1cm", │
│ MarginRight: "1cm", │
│ MarginBottom: "1cm", │
│ MarginLeft: "1cm", │
│ PrintBackground: true, // Include colors │
│ Scale: 1.0, │
│ Landscape: false, │
│ } │
└─────────────────────────────────────────────────────────┘
4. PDF GENERATION
┌─────────────────────────────────────────────────────────┐
│ pdfBytes, err := pdf.GeneratePDF(htmlContent, opts) │
│ if err != nil { │
│ return PDFGenerationError(err) │
│ } │
└─────────────────────────────────────────────────────────┘
5. RESPONSE
┌─────────────────────────────────────────────────────────┐
│ // Build filename │
│ filename := fmt.Sprintf("CV-%s-%s.pdf", │
│ cv.Personal.Name, req.Lang) │
│ filename = strings.ReplaceAll(filename, " ", "-") │
│ │
│ // Set headers │
│ w.Header().Set("Content-Type", "application/pdf") │
│ w.Header().Set("Content-Disposition", │
│ fmt.Sprintf("attachment; filename=%s", filename)) │
│ w.Header().Set("Content-Length", │
│ fmt.Sprintf("%d", len(pdfBytes))) │
│ │
│ // Send PDF │
│ w.WriteHeader(http.StatusOK) │
│ w.Write(pdfBytes) │
└─────────────────────────────────────────────────────────┘
```
## Chromedp PDF Generation
```
┌────────────────────────────────────────────────────────────┐
│ Chromedp PDF Generation (internal/pdf/generator.go) │
└────────────────────────────────────────────────────────────┘
func GeneratePDF(htmlContent string, opts Options) ([]byte, error) {
1. CREATE CONTEXT
┌──────────────────────────────────────────────────────┐
│ // Allocate context │
│ ctx, cancel := chromedp.NewContext( │
│ context.Background(), │
│ chromedp.WithLogf(log.Printf), │
│ ) │
│ defer cancel() │
│ │
│ // Set timeout │
│ ctx, cancel = context.WithTimeout(ctx, 30*time.Second) │
│ defer cancel() │
└──────────────────────────────────────────────────────┘
2. PREPARE HTML
┌──────────────────────────────────────────────────────┐
│ // Wrap HTML in data URL │
│ dataURL := fmt.Sprintf( │
│ "data:text/html;base64,%s", │
│ base64.StdEncoding.EncodeToString( │
│ []byte(htmlContent), │
│ ), │
│ ) │
└──────────────────────────────────────────────────────┘
3. LAUNCH CHROME
┌──────────────────────────────────────────────────────┐
│ // Run Chrome tasks │
│ var pdfBytes []byte │
│ err := chromedp.Run(ctx, │
│ // Navigate to data URL │
│ chromedp.Navigate(dataURL), │
│ │
│ // Wait for body to be ready │
│ chromedp.WaitReady("body", chromedp.ByQuery), │
│ │
│ // Wait for fonts and images │
│ chromedp.Sleep(500 * time.Millisecond), │
│ │
│ // Generate PDF │
│ chromedp.ActionFunc(func(ctx context.Context) error { │
│ buf, _, err := page.PrintToPDF(). │
│ WithPrintBackground(opts.PrintBackground). │
│ WithPaperWidth(opts.PaperWidth). │
│ WithPaperHeight(opts.PaperHeight). │
│ WithMarginTop(opts.MarginTop). │
│ WithMarginRight(opts.MarginRight). │
│ WithMarginBottom(opts.MarginBottom). │
│ WithMarginLeft(opts.MarginLeft). │
│ WithScale(opts.Scale). │
│ Do(ctx) │
│ if err != nil { │
│ return err │
│ } │
│ pdfBytes = buf │
│ return nil │
│ }), │
│ ) │
│ │
│ if err != nil { │
│ return nil, fmt.Errorf("chromedp: %w", err) │
│ } │
└──────────────────────────────────────────────────────┘
4. RETURN PDF
┌──────────────────────────────────────────────────────┐
│ return pdfBytes, nil │
└──────────────────────────────────────────────────────┘
}
```
## PDF-Specific Template Adjustments
```
┌────────────────────────────────────────────────────────────┐
│ PDF-Specific Template Adjustments │
└────────────────────────────────────────────────────────────┘
In templates/index.html:
{{if .IsPDF}}
<!-- PDF-specific styles -->
<style>
/* Hide interactive elements */
.toggle-button, .interactive-controls {
display: none !important;
}
/* Optimize for print */
body {
background: white !important;
}
/* Better page breaks */
.experience-item {
page-break-inside: avoid;
}
/* Consistent sizing */
.cv-section {
margin-bottom: 1.5cm;
}
/* Font optimization */
body {
font-size: 10pt;
line-height: 1.4;
}
</style>
{{else}}
<!-- Web-specific styles -->
<style>
.interactive-controls {
display: block;
}
</style>
{{end}}
```
## PDF Request/Response Example
```
┌────────────────────────────────────────────────────────────┐
│ PDF Request/Response Example │
└────────────────────────────────────────────────────────────┘
REQUEST:
POST /export/pdf HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Origin: http://localhost:8080
{
"lang": "es",
"length": "long",
"icons": "show",
"version": "with_skills"
}
RESPONSE (Success):
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="CV-John-Doe-es.pdf"
Content-Length: 245678
[PDF binary data]
RESPONSE (Error - Invalid Language):
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"success": false,
"error": {
"code": "INVALID_LANGUAGE",
"message": "Unsupported language: xx (use 'en' or 'es')",
"field": "lang"
}
}
RESPONSE (Error - Rate Limited):
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many PDF exports. Please wait a minute."
}
}
RESPONSE (Error - PDF Generation Failed):
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"success": false,
"error": {
"code": "PDF_GENERATION",
"message": "Failed to generate PDF. Please try again."
}
}
```
## PDF Options Structure
```
┌────────────────────────────────────────────────────────────┐
│ PDF Options (internal/pdf/options.go) │
└────────────────────────────────────────────────────────────┘
type Options struct {
// Paper settings
PaperSize PaperSize // A4, Letter, Legal
Orientation Orientation // Portrait, Landscape
PaperWidth float64 // In inches
PaperHeight float64 // In inches
// Margins
MarginTop string // "1cm", "0.5in"
MarginRight string
MarginBottom string
MarginLeft string
// Rendering
PrintBackground bool // Include background colors
Scale float64 // 0.5 to 2.0
Landscape bool // True for landscape
// Quality
PreferCSSPageSize bool
DisplayHeaderFooter bool
HeaderTemplate string
FooterTemplate string
}
Default A4 Options:
Options{
PaperSize: A4, // 8.27 x 11.69 inches
Orientation: Portrait,
MarginTop: "1cm",
MarginRight: "1cm",
MarginBottom: "1cm",
MarginLeft: "1cm",
PrintBackground: true,
Scale: 1.0,
Landscape: false,
}
```
## Rate Limiting
```
┌────────────────────────────────────────────────────────────┐
│ Rate Limiting for PDF Export │
└────────────────────────────────────────────────────────────┘
RateLimiter Middleware:
├─ 3 requests per minute per IP
├─ Uses token bucket algorithm
└─ Applied only to /export/pdf endpoint
Implementation:
type RateLimiter struct {
requests map[string]*bucket
mu sync.RWMutex
}
type bucket struct {
tokens int
lastReset time.Time
}
func (rl *RateLimiter) Allow(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
bucket := rl.requests[ip]
if bucket == nil {
bucket = &bucket{
tokens: 3,
lastReset: time.Now(),
}
rl.requests[ip] = bucket
}
// Reset bucket every minute
if time.Since(bucket.lastReset) > time.Minute {
bucket.tokens = 3
bucket.lastReset = time.Now()
}
// Check tokens
if bucket.tokens <= 0 {
return false // Rate limited
}
bucket.tokens--
return true
}
Response when rate limited:
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many PDF exports. Please wait a minute."
}
}
```
## PDF Performance
```
┌────────────────────────────────────────────────────────────┐
│ PDF Performance │
└────────────────────────────────────────────────────────────┘
Timing Breakdown:
┌─────────────────────────────────────────────────────────┐
│ Operation Time % │
├─────────────────────────────────────────────────────────┤
│ Request validation ~1ms 0.1% │
│ HTML generation ~50ms 5% │
│ Chrome launch ~200ms 20% │
│ Page navigation ~100ms 10% │
│ Font loading ~50ms 5% │
│ PDF rendering ~550ms 55% │
│ Response transmission ~50ms 5% │
├─────────────────────────────────────────────────────────┤
│ TOTAL ~1000ms 100% │
└─────────────────────────────────────────────────────────┘
Optimization Strategies:
1. Keep Chrome instance warm
└─→ Pre-launch Chrome on startup
Reuse context for multiple PDFs
2. Optimize HTML
└─→ Inline critical CSS
Remove unused styles
3. Font optimization
└─→ Use web-safe fonts
Preload font files
4. Cache templates
└─→ Pre-compile templates
Reuse parsed templates
5. Parallel processing
└─→ Queue PDF jobs
Process multiple concurrently
```
## Error Scenarios
```
┌────────────────────────────────────────────────────────────┐
│ PDF Error Scenarios │
└────────────────────────────────────────────────────────────┘
1. Chrome Launch Failed
Error: chromedp: failed to allocate context
Cause: Chrome not installed or crashed
Recovery: Log error, return 500, suggest retry
2. Timeout
Error: context deadline exceeded
Cause: PDF generation took > 30 seconds
Recovery: Cancel operation, return timeout error
3. Memory Limit
Error: out of memory
Cause: Too many concurrent PDF generations
Recovery: Rate limiting, queue system
4. Template Error
Error: template execution failed
Cause: Missing data or invalid template
Recovery: Fix template, ensure all data present
5. Navigation Error
Error: navigation failed
Cause: Invalid HTML or data URL too large
Recovery: Check HTML validity, reduce size
```
## Related Diagrams
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
- [Handler Organization](./04-handler-organization.md) - Handler structure
- [Error Handling Flow](./06-error-handling-flow.md) - Error handling
- [Template Rendering](./07-template-rendering.md) - Template system
+50
View File
@@ -0,0 +1,50 @@
# Architecture Diagrams
Visual representations of the CV website architecture, data flow, and component relationships.
## Available Diagrams
1. [System Architecture](./01-system-architecture.md) - Overall system design
2. [Request Flow](./02-request-flow.md) - HTTP request lifecycle
3. [Middleware Chain](./03-middleware-chain.md) - Middleware execution order
4. [Handler Organization](./04-handler-organization.md) - Handler file structure
5. [Data Models](./05-data-models.md) - CV and UI data structures
6. [Error Handling Flow](./06-error-handling-flow.md) - Error propagation and handling
7. [Template Rendering](./07-template-rendering.md) - Template compilation and rendering
8. [PDF Generation](./08-pdf-generation.md) - PDF export process
## Diagram Format
All diagrams are created using ASCII art for:
- Easy version control (text-based)
- Universal compatibility (no special tools needed)
- Fast loading and rendering
- Copy-paste friendly
## Reading Diagrams
```
┌─────┐
│ Box │ = Component or module
└─────┘
↓ = Data flow direction
┌─┬─┐
│A│B│ = Multiple components side by side
└─┴─┘
┌───────┐
│ ┌───┤ = Nested components
│ └───┘
└───────┘
```
## Conventions
- **Solid lines** (`─`, `│`): Direct dependencies
- **Arrows** (`→`, `↓`): Data flow direction
- **Boxes** (`┌─┐`): Components, modules, files
- **Double lines** (`═`, `║`): Important/critical paths
- **Dotted** (`:`, `.`): Optional or conditional paths
@@ -0,0 +1,425 @@
# Middleware Pattern in Go
## Pattern Overview
The Middleware Pattern wraps HTTP handlers to add cross-cutting concerns like logging, authentication, error recovery, and request preprocessing. It follows the decorator pattern, allowing you to compose multiple middleware into a chain.
## Pattern Structure
```go
// Middleware function signature
type Middleware func(http.Handler) http.Handler
// Middleware wraps a handler
func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing (before handler)
// ... do something before
// Call next handler
next.ServeHTTP(w, r)
// Post-processing (after handler)
// ... do something after
})
}
```
## Real Implementation from Project
### Preferences Middleware
```go
// internal/middleware/preferences.go
// PreferencesMiddleware reads user preference cookies and stores them in request context
func PreferencesMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing: Read cookies
prefs := &Preferences{
CVLength: getCookieWithDefault(r, "cv-length", "short"),
CVIcons: getCookieWithDefault(r, "cv-icons", "show"),
CVLanguage: getCookieWithDefault(r, "cv-language", "en"),
CVTheme: getCookieWithDefault(r, "cv-theme", "default"),
ColorTheme: getCookieWithDefault(r, "color-theme", "light"),
}
// Migrate old values
if prefs.CVLength == "extended" {
prefs.CVLength = "long"
}
// Store in context
ctx := context.WithValue(r.Context(), PreferencesKey, prefs)
// Call next handler with modified context
next.ServeHTTP(w, r.WithContext(ctx))
// No post-processing needed for this middleware
})
}
```
### Recovery Middleware
```go
// internal/middleware/recovery.go
// Recovery catches panics and returns 500 error
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Setup panic recovery
defer func() {
if err := recover(); err != nil {
// Log panic with stack trace
log.Printf("PANIC: %v\n%s", err, debug.Stack())
// Return error response
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// Call next handler (protected by defer/recover)
next.ServeHTTP(w, r)
})
}
```
### Logger Middleware
```go
// internal/middleware/logger.go
// Logger logs HTTP requests and their duration
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing: Start timer and log request
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
// Wrap ResponseWriter to capture status code
wrapped := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
// Call next handler
next.ServeHTTP(wrapped, r)
// Post-processing: Log duration and status
duration := time.Since(start)
log.Printf("Completed in %v (status: %d)", duration, wrapped.statusCode)
})
}
// Helper to capture response status
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
```
## Middleware Composition
### Chaining Middleware
```go
// internal/routes/routes.go
func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler {
mux := http.NewServeMux()
// Register routes
mux.HandleFunc("/", cvHandler.Home)
mux.HandleFunc("/cv", cvHandler.CVContent)
mux.HandleFunc("/health", healthHandler.Health)
// Compose middleware chain
// Execution order: Recovery → Logger → SecurityHeaders → Preferences → mux
handler := middleware.Recovery(
middleware.Logger(
middleware.SecurityHeaders(
middleware.PreferencesMiddleware(mux),
),
),
)
return handler
}
```
### Route-Specific Middleware
```go
// Apply middleware only to specific routes
func Setup(cvHandler *handlers.CVHandler) http.Handler {
mux := http.NewServeMux()
// Public routes (minimal middleware)
mux.HandleFunc("/", cvHandler.Home)
mux.HandleFunc("/health", healthHandler.Health)
// Protected PDF route (additional middleware)
pdfHandler := middleware.OriginChecker(
middleware.RateLimiter(
http.HandlerFunc(cvHandler.ExportPDF),
3, // 3 requests per minute
),
)
mux.Handle("/export/pdf", pdfHandler)
// Global middleware for all routes
handler := middleware.Recovery(
middleware.Logger(
middleware.PreferencesMiddleware(mux),
),
)
return handler
}
```
## Common Middleware Use Cases
### 1. Authentication
```go
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get token from header
token := r.Header.Get("Authorization")
// Validate token
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Store user ID in context
ctx := context.WithValue(r.Context(), UserIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
### 2. CORS
```go
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// Handle preflight
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
```
### 3. Request Timeout
```go
func Timeout(duration time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create context with timeout
ctx, cancel := context.WithTimeout(r.Context(), duration)
defer cancel()
// Create channel for handler completion
done := make(chan struct{})
go func() {
next.ServeHTTP(w, r.WithContext(ctx))
close(done)
}()
// Wait for completion or timeout
select {
case <-done:
// Handler completed
case <-ctx.Done():
// Timeout occurred
http.Error(w, "Request Timeout", http.StatusGatewayTimeout)
}
})
}
}
```
### 4. Request ID
```go
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Generate unique ID
requestID := uuid.New().String()
// Add to response header
w.Header().Set("X-Request-ID", requestID)
// Store in context
ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
## Middleware Execution Flow
```
Request
┌─────────────────────────┐
│ Recovery Middleware │ ← Outermost (catches all panics)
│ defer/recover │
└─────────────────────────┘
┌─────────────────────────┐
│ Logger Middleware │ ← Logs request + duration
│ Pre: Log request │
│ Post: Log duration │
└─────────────────────────┘
┌─────────────────────────┐
│ Security Middleware │ ← Add security headers
│ Set headers │
└─────────────────────────┘
┌─────────────────────────┐
│ Preferences Middleware │ ← Innermost (closest to handler)
│ Read cookies → context │
└─────────────────────────┘
┌─────────────────────────┐
│ Handler │ ← Business logic
│ Process request │
└─────────────────────────┘
Response (unwraps in reverse order)
```
## Benefits
1. **Separation of Concerns**: Cross-cutting logic separate from handlers
2. **Composability**: Chain multiple middleware together
3. **Reusability**: Same middleware for multiple routes
4. **Testability**: Easy to test in isolation
5. **Maintainability**: Change behavior without touching handlers
## Best Practices
### ✅ DO
```go
// Keep middleware focused on one concern
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only logging logic here
log.Printf("[%s] %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
// Use context for request-scoped values
func PreferencesMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prefs := readPreferences(r)
ctx := context.WithValue(r.Context(), PrefsKey, prefs)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Order middleware correctly (outer to inner)
handler := Recovery(Logger(Auth(mux)))
```
### ❌ DON'T
```go
// DON'T mix multiple concerns in one middleware
func BadMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Too much! Logging, auth, CORS, caching...
log.Print(r.URL)
if !checkAuth(r) { return }
w.Header().Set("Access-Control-Allow-Origin", "*")
cached := getCache(r.URL.Path)
// ...
})
}
// DON'T store context in struct
type BadMiddleware struct {
ctx context.Context // Wrong!
}
// DON'T modify original request (use r.WithContext)
func BadMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Set("X-Foo", "bar") // Modifies original!
next.ServeHTTP(w, r)
})
}
```
## Testing Middleware
```go
func TestPreferencesMiddleware(t *testing.T) {
// Create test handler that reads preferences
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prefs := GetPreferences(r)
if prefs.CVLength != "long" {
t.Errorf("expected long, got %s", prefs.CVLength)
}
w.WriteHeader(http.StatusOK)
})
// Wrap with middleware
wrapped := PreferencesMiddleware(handler)
// Create test request with cookie
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "cv-length", Value: "long"})
// Execute
w := httptest.NewRecorder()
wrapped.ServeHTTP(w, req)
// Verify
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
```
## Related Patterns
- **Chain of Responsibility**: Middleware is a specific implementation
- **Decorator Pattern**: Wrapping handlers adds behavior
- **Context Pattern**: Often used together for request-scoped data
## Further Reading
- [Writing Middleware in Go](https://www.alexedwards.net/blog/making-and-using-middleware)
- [Middleware Pattern in Go](https://gowebexamples.com/advanced-middleware/)
- [Context Pattern](./03-context-pattern.md) - Used with middleware
+219
View File
@@ -0,0 +1,219 @@
# Go Patterns Used in This Project
This directory contains documentation on the Go design patterns and idioms used throughout the CV website project.
## Pattern Catalog
1. **[Middleware Pattern](./01-middleware-pattern.md)** - HTTP middleware chain for cross-cutting concerns
2. **[Handler Pattern](./02-handler-pattern.md)** - Organized HTTP handler structure
3. **[Context Pattern](./03-context-pattern.md)** - Request-scoped values using context
4. **[Error Wrapping](./04-error-wrapping.md)** - Structured error handling with wrapping
5. **[Dependency Injection](./05-dependency-injection.md)** - Constructor-based dependency injection
6. **[Template Pattern](./06-template-pattern.md)** - Cached template management
7. **[Singleton Pattern](./07-singleton-pattern.md)** - Single instance managers (template, config)
8. **[Factory Pattern](./08-factory-pattern.md)** - Error and response constructors
## Pattern Categories
### Structural Patterns
- **Middleware Pattern** - Composable request processing
- **Singleton Pattern** - Single instance coordination
- **Dependency Injection** - Decoupled component initialization
### Behavioral Patterns
- **Handler Pattern** - Request routing and handling
- **Context Pattern** - Request-scoped data propagation
- **Template Pattern** - Flexible rendering engine
### Error Handling Patterns
- **Error Wrapping** - Context-rich error chains
- **Typed Errors** - Domain-specific error types
- **Factory Pattern** - Consistent error creation
## Pattern Usage Map
```
┌────────────────────────────────────────────────────────────┐
│ Pattern Usage Map │
└────────────────────────────────────────────────────────────┘
main.go
├─→ Singleton Pattern (config, template manager)
├─→ Dependency Injection (handler construction)
└─→ Middleware Pattern (chain setup)
internal/handlers/
├─→ Handler Pattern (method organization)
├─→ Error Wrapping (error handling)
├─→ Factory Pattern (error/response creation)
└─→ Context Pattern (preference access)
internal/middleware/
├─→ Middleware Pattern (http.Handler wrapping)
├─→ Context Pattern (value storage)
└─→ Error Wrapping (panic recovery)
internal/templates/
├─→ Singleton Pattern (manager instance)
├─→ Template Pattern (rendering strategy)
└─→ Dependency Injection (config injection)
internal/models/
├─→ Factory Pattern (model loading)
└─→ Error Wrapping (validation errors)
```
## When to Use Each Pattern
### Middleware Pattern
✓ Cross-cutting concerns (logging, auth, CORS)
✓ Request/response modification
✓ Chain-of-responsibility needs
✗ Business logic (use handlers instead)
### Handler Pattern
✓ HTTP request handling
✓ Route-specific logic
✓ Organizing endpoints by resource
✗ Generic utilities (use packages instead)
### Context Pattern
✓ Request-scoped values (user, preferences)
✓ Cancellation signals
✓ Deadlines and timeouts
✗ Function parameters (use explicit params)
### Error Wrapping
✓ Adding context to errors
✓ Preserving error chains
✓ Debug information
✗ Simple errors (use errors.New)
### Dependency Injection
✓ Decoupling components
✓ Testing with mocks
✓ Configuration flexibility
✗ Simple functions (use direct calls)
### Template Pattern
✓ Flexible rendering
✓ HTML generation
✓ Hot reload in development
✗ JSON APIs (use direct encoding)
### Singleton Pattern
✓ Shared resources (DB, cache)
✓ Configuration managers
✓ Template engines
✗ Stateless utilities (use packages)
### Factory Pattern
✓ Complex object creation
✓ Consistent initialization
✓ Error construction
✗ Simple structs (use literals)
## Anti-Patterns to Avoid
### ❌ Global State
```go
// BAD: Mutable global variable
var globalConfig Config
// GOOD: Pass as dependency
func NewHandler(config *Config) *Handler
```
### ❌ Panic for Flow Control
```go
// BAD: Using panic for expected errors
if err != nil {
panic(err)
}
// GOOD: Return errors
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
```
### ❌ Ignoring Errors
```go
// BAD: Ignoring error
_ = json.Unmarshal(data, &result)
// GOOD: Handle error
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("unmarshal: %w", err)
}
```
### ❌ Context in Structs
```go
// BAD: Storing context in struct
type Handler struct {
ctx context.Context
}
// GOOD: Pass context as first parameter
func (h *Handler) Handle(ctx context.Context, w, r)
```
### ❌ Naked Returns
```go
// BAD: Naked return with named results
func process() (result string, err error) {
result = "foo"
return // Confusing!
}
// GOOD: Explicit return
func process() (string, error) {
result := "foo"
return result, nil
}
```
## Learning Path
For developers new to these patterns:
1. **Start with**: Handler Pattern, Error Wrapping
2. **Then learn**: Middleware Pattern, Context Pattern
3. **Advanced**: Dependency Injection, Template Pattern
4. **Master**: Singleton Pattern, Factory Pattern
## Resources
- [Effective Go](https://golang.org/doc/effective_go) - Official Go style guide
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) - Common mistakes
- [Practical Go](https://dave.cheney.net/practical-go) - Best practices
## Pattern Evolution
This project evolved through these pattern adoptions:
### Phase 1: Basic Structure
- Simple handlers
- No middleware
- Manual cookie reading
### Phase 2: Middleware Introduction
- PreferencesMiddleware added
- Cookie handling centralized
- Context pattern adopted
### Phase 3: Type Safety
- Request/response types
- Validation tags
- Typed errors
### Phase 4: Error Handling
- Error wrapping throughout
- Domain error types
- Centralized error handler
### Phase 5: Testing
- Dependency injection for testability
- Mock-friendly interfaces
- Benchmark tests