package handlers import ( "bytes" "log" "net/http" "strings" "time" c "github.com/juanatsap/cv-site/internal/constants" "github.com/juanatsap/cv-site/internal/httputil" "github.com/juanatsap/cv-site/internal/middleware" ) // ============================================================================== // STREAMING HANDLERS // HTML Streaming for progressive page rendering // ============================================================================== // HomeStreaming renders the CV page using HTTP streaming for faster perceived load // Strategy: Stream the
and initial skeleton immediately, then stream body content // Falls back to classic Home handler if streaming not supported func (h *CVHandler) HomeStreaming(w http.ResponseWriter, r *http.Request) { startTime := time.Now() // Check if this is a shortcut URL request (cv-jamr-{year}-{lang}.pdf) if strings.HasPrefix(r.URL.Path, "/cv-jamr-") && strings.HasSuffix(r.URL.Path, ".pdf") { h.DefaultCVShortcut(w, r) return } // Detect text-based browsers and serve plain text version if isTextBrowser(r) { h.PlainText(w, r) return } // Check if response writer supports flushing flusher, ok := w.(http.Flusher) if !ok { // Streaming not supported - fallback to classic rendering log.Printf("[streaming] Flusher not supported, falling back to classic handler") h.Home(w, r) return } // Validate language lang, ok := httputil.LangOrError(r) if !ok { HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'")) return } // Get user preferences from context prefs := middleware.GetPreferences(r) // ================================================================== // CHUNK 1: Send minimal loading indicator immediately // ================================================================== w.Header().Set(c.HeaderContentType, c.ContentTypeHTML) w.Header().Set("Transfer-Encoding", "chunked") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("Cache-Control", "no-cache") // Send loading indicator that will be replaced loadingHTML := generateLoadingHTML(lang) if _, err := w.Write([]byte(loadingHTML)); err != nil { log.Printf("[streaming] Error writing loading HTML: %v", err) return } flusher.Flush() skeletonTime := time.Since(startTime) log.Printf("[streaming] Loading indicator sent in %v", skeletonTime) // ================================================================== // CHUNK 2: Prepare data (parallel git operations happen here) // ================================================================== data, err := h.prepareTemplateData(lang) if err != nil { // Error after headers sent - send inline error errorHTML := `` _, _ = w.Write([]byte(errorHTML)) flusher.Flush() return } // Add preference-specific fields cvLengthClass := "cv-short" if prefs.CVLength == "long" { cvLengthClass = "cv-long" } data["CVLengthClass"] = cvLengthClass data["ShowIcons"] = (prefs.CVIcons == "show") data["ThemeClean"] = (prefs.CVTheme == "clean") dataTime := time.Since(startTime) log.Printf("[streaming] Data prepared in %v (delta: %v)", dataTime, dataTime-skeletonTime) // ================================================================== // CHUNK 3: Render full template and stream it // ================================================================== tmpl, err := h.templates.Render("index.html") if err != nil { errorHTML := `` _, _ = w.Write([]byte(errorHTML)) flusher.Flush() return } var contentBuf bytes.Buffer if err := tmpl.Execute(&contentBuf, data); err != nil { errorHTML := `` _, _ = w.Write([]byte(errorHTML)) flusher.Flush() return } // Replace the entire document with the full rendered content // This preserves all body attributes including hyperscript handlers fullHTML := contentBuf.String() // Escape the HTML for JavaScript string escapedHTML := escapeForJS(fullHTML) // Script inside body that replaces entire document // We need to use a DOMParser approach since document.write() doesn't work reliably // after the initial document has been parsed replaceScript := `