# Frontend Replacement Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the Go template-based frontend with the search-zen-50 React SPA, embedded in the Go binary as a single deployment. **Architecture:** Build React app → embed in Go binary via `//go:embed` → serve via Go HTTP server with SPA fallback routing. React calls `/search?format=json` and `/autocompleter?q=` APIs. **Tech Stack:** Go (embed), React 18, Vite, TailwindCSS, React Router, @tanstack/react-query --- ## File Map | File | Action | |------|--------| | `cmd/kafka/main.go` | Modify - replace template handlers with SPA handler | | `internal/spa/spa.go` | Create - embed React build, serve static files, SPA fallback | | `internal/spa/dist/` | Build output - React build artifacts (gitignored) | | `src/hooks/use-search.ts` | Modify - replace mock with real API calls | | `src/lib/mock-data.ts` | Keep types, remove MOCK_RESPONSE usage | --- ## Task 1: Build React App **Files:** - Build: `/tmp/search-zen-50/dist/` (output directory) - [ ] **Step 1: Install dependencies and build** ```bash cd /tmp/search-zen-50 && bun install && bun run build ``` Expected: `dist/` directory created with `index.html`, `assets/` folder containing JS/CSS bundles - [ ] **Step 2: Verify dist contents** ```bash ls /tmp/search-zen-50/dist/ && ls /tmp/search-zen-50/dist/assets/ | head -10 ``` Expected: `index.html` exists, `assets/` contains `.js` and `.css` files --- ## Task 2: Create SPA Go Package **Files:** - Create: `internal/spa/spa.go` ```go package spa import ( "embed" "io/fs" "net/http" "path" ) //go:embed all:dist var distFS embed.FS // DistFS returns the embedded dist directory as an fs.FS. func DistFS() (fs.FS, error) { return fs.Sub(distFS, "dist") } // NewHandler returns an HTTP handler that: // - Serves static files from the embedded dist/ directory // - Falls back to index.html for SPA routing (any non-API path) func NewHandler() http.Handler { dist, err := DistFS() if err != nil { panic("spa: embedded dist not found: " + err.Error()) } return &spaHandler{dist: dist} } type spaHandler struct { dist fs.FS } func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // API paths are handled by Go API handlers - this should never be reached // since Go mux dispatches to specific handlers first. But if reached, // pass through to FileServer which will return 404 for unknown paths. // Try to serve the requested file first filePath := path.Clean(r.URL.Path) f, err := h.dist.Open(filePath) if err == nil { f.Close() // File exists - serve it via FileServer http.FileServer(http.FS(h.dist)).ServeHTTP(w, r) return } // Fallback to index.html for SPA routing indexFile, err := h.dist.Open("index.html") if err != nil { http.Error(w, "index.html not found in embedded files", http.StatusInternalServerError) return } indexFile.Close() http.FileServer(http.FS(h.dist)).ServeHTTP(w, r) } ``` --- ## Task 3: Wire SPA Handler in main.go **Files:** - Modify: `cmd/kafka/main.go` - [ ] **Step 1: Replace handlers with SPA** In `main.go`, find and replace the `mux.HandleFunc` section (lines 82-88) and the static file serving section (lines 90-96). Old code (lines 82-96): ```go mux := http.NewServeMux() mux.HandleFunc("/", h.Index) mux.HandleFunc("/healthz", h.Healthz) mux.HandleFunc("/search", h.Search) mux.HandleFunc("/autocompleter", h.Autocompleter) mux.HandleFunc("/preferences", h.Preferences) mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL)) // Serve embedded static files (CSS, JS, images). staticFS, err := views.StaticFS() if err != nil { log.Fatalf("failed to load static files: %v", err) } var subFS fs.FS = staticFS mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS)))) ``` New code: ```go mux := http.NewServeMux() // API routes - handled by Go mux.HandleFunc("/healthz", h.Healthz) mux.HandleFunc("/search", h.Search) mux.HandleFunc("/autocompleter", h.Autocompleter) mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL)) // SPA handler - serves React app for all other routes spaHandler := spa.NewHandler() mux.Handle("/", spaHandler) ``` - [ ] **Step 2: Add spa import** Add to imports (after `"github.com/metamorphosis-dev/kafka/internal/search"`): ```go "github.com/metamorphosis-dev/kafka/internal/spa" ``` - [ ] **Step 3: Remove unused views import if needed** If `views` is only used for `StaticFS()`, remove the import. The template rendering functions (`RenderIndex`, etc.) won't be needed anymore. - [ ] **Step 4: Verify build** ```bash cd /home/ashie/git/kafka && go build ./cmd/kafka/ ``` Expected: Builds successfully (may fail on embed if dist not found - continue to next task) --- ## Task 4: Wire React to Real API **Files:** - Modify: `src/hooks/use-search.ts` in `/tmp/search-zen-50/` - [ ] **Step 1: Replace mock search with real API call** Replace the `search` function in `use-search.ts`: Old code (lines 23-36): ```typescript const search = useCallback(async (query: string) => { if (!query.trim()) return; setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true })); // Simulate network delay await new Promise((r) => setTimeout(r, 800)); setState((prev) => ({ ...prev, isLoading: false, results: { ...MOCK_RESPONSE, query }, })); }, []); ``` New code: ```typescript const search = useCallback(async (query: string) => { if (!query.trim()) return; setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true })); try { const response = await fetch(`/search?format=json&q=${encodeURIComponent(query)}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); setState((prev) => ({ ...prev, isLoading: false, results: data, })); } catch (err) { setState((prev) => ({ ...prev, isLoading: false, error: err instanceof Error ? err.message : "Search failed", })); } }, []); ``` - [ ] **Step 2: Remove mock data import** Remove the mock import line (should be near line 2): ```typescript import { MOCK_RESPONSE, type SearXNGResponse, type Category } from "@/lib/mock-data"; ``` Replace with: ```typescript import type { SearXNGResponse, Category } from "@/lib/mock-data"; ``` - [ ] **Step 3: Keep the CATEGORIES export** Ensure `mock-data.ts` still exports `CATEGORIES` and `Category` type. The file should look like: ```typescript // Keep these exports - used by CategoryTabs and preferences export const CATEGORIES = ["general", "it", "images", "news"] as const; export type Category = typeof CATEGORIES[number]; // Keep interfaces export interface SearchResult { url: string; title: string; content: string; engine: string; parsed_url: [string, string, string, string, string]; engines: string[]; positions: number[]; score: number; category: string; pretty_url: string; img_src?: string; thumbnail?: string; publishedDate?: string; } export interface SearXNGResponse { query: string; number_of_results: number; results: SearchResult[]; answers: string[]; corrections: string[]; infoboxes: any[]; suggestions: string[]; unresponsive_engines: string[]; } ``` --- ## Task 5: Rebuild React and Verify **Files:** - Build: `/tmp/search-zen-50/dist/` - [ ] **Step 1: Rebuild with changes** ```bash cd /tmp/search-zen-50 && bun run build ``` - [ ] **Step 2: Copy dist to kafka** ```bash rm -rf /home/ashie/git/kafka/internal/spa/dist cp -r /tmp/search-zen-50/dist /home/ashie/git/kafka/internal/spa/dist ``` - [ ] **Step 3: Verify Go build** ```bash cd /home/ashie/git/kafka && go build ./cmd/kafka/ && echo "Build successful" ``` Expected: "Build successful" --- ## Task 6: Test the Integration - [ ] **Step 1: Start the server** ```bash cd /home/ashie/git/kafka && ./kafka -config config.toml & sleep 2 ``` - [ ] **Step 2: Test homepage** ```bash curl -s http://localhost:8080/ | head -20 ``` Expected: HTML with `
` from React app - [ ] **Step 3: Test API** ```bash curl -s "http://localhost:8080/search?format=json&q=test" | head -50 ``` Expected: JSON search response - [ ] **Step 4: Clean up** ```bash pkill -f "./kafka" 2>/dev/null; echo "Done" ``` --- ## Dependencies - Node.js/Bun for building React app - Go 1.24+ for embed functionality - No new Go dependencies ## Notes - The `internal/spa/dist/` folder should be gitignored (build artifact) - The `internal/spa/dist/` copy is needed for the embed to work at compile time - Preferences page is entirely client-side (localStorage) - no backend needed - Autocomplete can be added later by modifying `SearchInput.tsx` to call `/autocompleter`