# Irgo Framework - AI Assistant Guide > This document instructs AI coding assistants on how to write applications using the Irgo framework. ## Framework Overview Irgo is a mobile application framework that uses: - **Go** as the backend runtime (compiled via gomobile) - **HTMX** for frontend interactivity (hypermedia-driven) - **templ** for type-safe HTML templates - **WebView** to render the UI (iOS WKWebView, Android WebView) The key insight: there's no network - HTTP requests from HTMX are intercepted by the WebView and routed directly to Go handlers running in-process. ## Core Principles 1. **Hypermedia-Driven**: Return HTML fragments, not JSON. HTMX swaps HTML directly into the DOM. 2. **Server-Side State**: All state lives in Go. The WebView is a thin rendering layer. 3. **Progressive Enhancement**: Start with working HTML, enhance with HTMX attributes. 4. **Type Safety**: Use templ for compile-time template checking. ## Project Structure When working on an Irgo project, expect this structure: ``` project/ ├── main.go # Entry point, dev server ├── app/app.go # Router setup ├── handlers/ # HTTP handlers (business logic) ├── templates/ # templ files (.templ) ├── static/ # CSS, JS, images ├── mobile/mobile.go # Mobile bridge └── ios/ # Xcode project ``` ## Writing Handlers ### Handler Signature All handlers follow this pattern: ```go func(ctx *router.Context) (string, error) ``` - `ctx` provides request data and response helpers - Return HTML string and optional error - Empty string return is valid (for DELETE operations, etc.) ### Handler Examples ```go // GET handler - return rendered template r.GET("/", func(ctx *router.Context) (string, error) { return renderer.Render(templates.HomePage()) }) // GET with URL parameter r.GET("/items/{id}", func(ctx *router.Context) (string, error) { id := ctx.Param("id") item, err := getItem(id) if err != nil { return renderer.Render(templates.NotFound()) } return renderer.Render(templates.ItemDetail(item)) }) // POST with form data r.POST("/items", func(ctx *router.Context) (string, error) { name := ctx.FormValue("name") item := createItem(name) return renderer.Render(templates.ItemRow(item)) }) // DELETE that triggers HTMX event r.DELETE("/items/{id}", func(ctx *router.Context) (string, error) { id := ctx.Param("id") deleteItem(id) ctx.Trigger("itemDeleted") return "", nil }) ``` ### Context Methods ```go ctx.Param("name") // URL path parameter: /users/{name} ctx.Query("key") // Query parameter: ?key=value ctx.FormValue("field") // Form field value ctx.Header("X-Custom") // Request header ctx.IsHTMX() // True if HX-Request header present ctx.SetHeader("key", "val") // Set response header ctx.Trigger("event") // Set HX-Trigger header ctx.Redirect("/path") // Redirect (HTMX-aware) ``` ## Writing Templates ### Template Syntax (templ) templ uses Go-like syntax with HTML: ```go package templates // Component with parameters templ Button(text string, href string) { { text } } // Component with children templ Card(title string) {

{ title }

{ children... }
} // Using components templ HomePage() { @Layout("Home") { @Card("Welcome") {

Hello, world!

@Button("Learn More", "/about") } } } // Conditional rendering templ UserStatus(user *User) { if user != nil { Welcome, { user.Name } } else { Sign In } } // Loops templ ItemList(items []Item) { } // Conditional classes templ TodoItem(todo Todo) {
{ todo.Title }
} // Dynamic attributes templ Input(name string, value string, required bool) { } ``` ### Layout Pattern Always use a layout component for full pages: ```go templ Layout(title string) { { title } { children... } } templ AboutPage() { @Layout("About") {

About Us

} } ``` ### Fragment Pattern For HTMX partial updates, return just the fragment: ```go // Full page (initial load or non-HTMX request) templ TodosPage(todos []Todo) { @Layout("Todos") {

My Todos

@TodoList(todos) @AddTodoForm()
} } // Fragment (for HTMX swaps) templ TodoList(todos []Todo) {
for _, todo := range todos { @TodoItem(todo) }
} // Single item fragment templ TodoItem(todo Todo) {
{ todo.Title }
} ``` ## HTMX Patterns ### Basic HTMX Attributes ```html
``` ### Common hx-swap Values - `innerHTML` (default): Replace inner HTML of target - `outerHTML`: Replace entire target element - `beforeend`: Append to target - `afterbegin`: Prepend to target - `delete`: Remove target element - `none`: Don't swap (for side-effect-only requests) ### Common hx-trigger Values - `click` (default for buttons) - `submit` (default for forms) - `change`: On input change - `keyup`: On key release - `load`: On element load - `revealed`: When element enters viewport - `every 5s`: Polling interval ### hx-boost for Navigation Enable SPA-like navigation: ```html
``` ### Loading Indicators ```html ``` CSS for indicators: ```css .htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: inline; } .htmx-request.htmx-indicator { display: inline; } ``` ### Event Triggers from Server In handler: ```go ctx.Trigger("itemCreated") // or with data: ctx.TriggerWithData("itemCreated", map[string]any{"id": 123}) ``` In HTML: ```html
``` ### Out-of-Band Swaps Update multiple elements from one response: ```go templ CreateItemResponse(item Item, count int) { // Primary response @ItemRow(item) // Out-of-band update { strconv.Itoa(count) } items } ``` ## Common Patterns ### CRUD Operations ```go // handlers/items.go package handlers type Item struct { ID string Name string } var items = make(map[string]Item) func MountItems(r *router.Router, renderer *render.TemplRenderer) { // List r.GET("/items", func(ctx *router.Context) (string, error) { list := getItems() if ctx.IsHTMX() { return renderer.Render(templates.ItemList(list)) } return renderer.Render(templates.ItemsPage(list)) }) // Create r.POST("/items", func(ctx *router.Context) (string, error) { name := ctx.FormValue("name") item := createItem(name) return renderer.Render(templates.ItemRow(item)) }) // Read r.GET("/items/{id}", func(ctx *router.Context) (string, error) { id := ctx.Param("id") item, ok := items[id] if !ok { ctx.SetStatus(404) return renderer.Render(templates.NotFound()) } return renderer.Render(templates.ItemDetail(item)) }) // Update r.PUT("/items/{id}", func(ctx *router.Context) (string, error) { id := ctx.Param("id") name := ctx.FormValue("name") item := updateItem(id, name) return renderer.Render(templates.ItemRow(item)) }) // Delete r.DELETE("/items/{id}", func(ctx *router.Context) (string, error) { id := ctx.Param("id") deleteItem(id) return "", nil // Empty response, HTMX will delete target }) } ``` ### Search/Filter ```go // templates/search.templ templ SearchForm() {
} // handlers/search.go r.GET("/search", func(ctx *router.Context) (string, error) { query := ctx.Query("q") results := search(query) return renderer.Render(templates.SearchResults(results)) }) ``` ### Infinite Scroll ```go templ ItemListWithPagination(items []Item, page int, hasMore bool) { for _, item := range items { @ItemRow(item) } if hasMore {
Loading more...
} } ``` ### Tabs ```go templ TabContainer() {
@TabOne()
} ``` ### Modal Dialog ```go templ ModalTrigger() { } templ Modal(title string) { } templ ConfirmModal() { @Modal("Confirm Action") {

Are you sure?

} } ``` ### Form Validation ```go templ FormField(name, label, value, error string) {
{ error }
} // Handler r.POST("/validate/email", func(ctx *router.Context) (string, error) { email := ctx.FormValue("email") if !isValidEmail(email) { return "Invalid email address", nil } return "", nil // Clear error }) ``` ## WebSocket Patterns Irgo supports real-time updates via WebSockets using HTMX 4's `hx-ws` extension. Use WebSockets for live dashboards, notifications, chat, and server-push updates. ### Layout Requirement The layout must include the hx-ws.js script (downloaded automatically by `irgo new`): ```html ``` Once loaded, use `hx-ws:connect` directly - no `hx-ext="ws"` attribute needed on elements. ### WebSocket Handler Pattern ```go // handlers/websocket.go package handlers import ( "encoding/json" "net/http" "sync" "github.com/gorilla/websocket" ) var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } var ( clients = make(map[*websocket.Conn]bool) clientsMu sync.RWMutex ) // WSEnvelope is the HTMX 4 WebSocket message format type WSEnvelope struct { Channel string `json:"channel,omitempty"` // "ui" for HTML updates Format string `json:"format,omitempty"` // "html" for HTML content Target string `json:"target,omitempty"` // CSS selector Swap string `json:"swap,omitempty"` // Swap strategy Payload string `json:"payload"` // HTML content RequestID string `json:"request_id,omitempty"` // For request-response matching } // WebSocketHandler handles WebSocket connections func WebSocketHandler(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() clientsMu.Lock() clients[conn] = true clientsMu.Unlock() defer func() { clientsMu.Lock() delete(clients, conn) clientsMu.Unlock() }() for { _, message, err := conn.ReadMessage() if err != nil { break } handleMessage(conn, message) } } // Broadcast sends HTML to all connected clients func Broadcast(html string) { env := WSEnvelope{Channel: "ui", Format: "html", Payload: html} data, _ := json.Marshal(env) clientsMu.RLock() defer clientsMu.RUnlock() for client := range clients { client.WriteMessage(websocket.TextMessage, data) } } ``` ### Registering WebSocket Endpoint In main.go: ```go mux.HandleFunc("/ws/updates", handlers.WebSocketHandler) ``` ### Connecting from Template ```go templ LiveDashboard() { @Layout("Dashboard") { // hx-ws:connect establishes connection (extension auto-loads)

Live Dashboard

// Elements with IDs receive updates when server sends matching HTML

Connecting...

} } ``` ### Sending Data to Server Use `hx-ws:send` to send data from client to server: ```go templ Counter() {
0
} templ ChatForm() {
} ``` ### Server → Client Message Format HTMX 4 expects JSON with this structure: ```json { "channel": "ui", "format": "html", "target": "#element-id", "swap": "innerHTML", "payload": "
HTML content
", "request_id": "optional-id" } ``` **Minimal** (uses defaults): ```json {"payload": "
Content
"} ``` ### Client → Server Message Format When `hx-ws:send` is triggered, HTMX sends: ```json { "type": "request", "request_id": "unique-id", "event": "click", "headers": {"HX-Request": "true", "HX-Trigger": "btn-id"}, "values": {"action": "increment"}, "path": "wss://example.com/ws", "id": "btn-id" } ``` Handle in Go: ```go type WSRequest struct { Type string `json:"type"` RequestID string `json:"request_id"` Event string `json:"event"` Values map[string]any `json:"values"` } func handleMessage(conn *websocket.Conn, message []byte) { var req WSRequest json.Unmarshal(message, &req) // Process action from req.Values // Send response with matching RequestID } ``` ### Broadcasting Updates ```go func StartStatsBroadcaster() { go func() { for { html, _ := renderer.Render(templates.StatsPanel(getStats())) handlers.Broadcast(html) time.Sleep(1 * time.Second) } }() } ``` ### Real-Time Stats Example ```go templ StatsPanel(stats Stats) {
CPU
{ fmt.Sprintf("%d%%", stats.CPU) }
Memory
{ fmt.Sprintf("%d%%", stats.Memory) }
} templ StatsPage() { @Layout("Stats") {
Loading...
} } ``` ### Out-of-Band Updates Update multiple elements from one broadcast: ```go templ MultiUpdate(stats Stats) { // Primary element (matched by ID)
...
// Out-of-band update to notifications
New update!
} ``` ### WebSocket Notifications ```go templ NotificationToast(message, msgType string) {
{ message }
} func notifyAll(message string) { html, _ := renderer.Render(templates.NotificationToast(message, "success")) handlers.Broadcast(html) } ``` ### WebSocket Key Points 1. **Include `hx-ws.js` script** in layout head 2. **Use `hx-ws:connect="/path"`** to establish connection (no `hx-ext` needed) 3. **Use `hx-ws:send`** to send data to server 4. **Server sends JSON envelope** with `payload` containing HTML 5. **Element IDs in payload** determine where content is swapped 6. **Use `hx-swap-oob`** for out-of-band updates to multiple elements 7. **Use `request_id`** in response to target the requesting element 8. **Add gorilla/websocket**: `go get github.com/gorilla/websocket` ## Mobile-Specific Patterns ### Safe Area Handling ```go templ MobileLayout(title string) { { children... } } ``` ### Touch-Friendly Buttons ```go templ MobileButton(text string) { } ``` ### Pull to Refresh ```go templ PullToRefresh(contentPath string) {
{ children... }
} ``` ## Do's and Don'ts ### DO: 1. **Return HTML fragments** from handlers, not JSON 2. **Use templ** for all HTML generation 3. **Use semantic HTML** (buttons for actions, anchors for navigation) 4. **Use hx-target** to specify where responses go 5. **Use hx-swap** to control how content is inserted 6. **Use ctx.Trigger()** to communicate events to the page 7. **Keep handlers simple** - one responsibility each 8. **Use Layout for full pages**, fragments for partial updates ### DON'T: 1. **Don't return JSON** - this is hypermedia, not an API 2. **Don't use JavaScript frameworks** - HTMX handles interactivity 3. **Don't manipulate DOM in JavaScript** - let HTMX do it 4. **Don't store state in JavaScript** - keep it in Go 5. **Don't use onclick for HTMX actions** - use hx-* attributes 6. **Don't forget hx-target** - know where your response goes 7. **Don't return full pages for HTMX requests** - return fragments ## File Naming Conventions - Handlers: `handlers/*.go` (e.g., `handlers/items.go`) - Templates: `templates/*.templ` (e.g., `templates/items.templ`) - Generated: `templates/*_templ.go` (auto-generated, don't edit) - Layouts: `templates/layout.templ` - Components: `templates/components.templ` - Pages: `templates/.templ` (e.g., `templates/home.templ`) ## Testing Patterns ```go // handlers/items_test.go func TestCreateItem(t *testing.T) { r := setupTestRouter() req := httptest.NewRequest("POST", "/items", strings.NewReader("name=Test")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != 200 { t.Errorf("expected 200, got %d", w.Code) } if !strings.Contains(w.Body.String(), "Test") { t.Error("response should contain item name") } } ``` ## Debugging Tips 1. **Check Safari Web Inspector** in dev mode for network/console 2. **Use fmt.Println** in handlers (shows in terminal) 3. **Check generated `_templ.go` files** if templates aren't working 4. **Verify hx-target exists** on the page 5. **Check browser console** for HTMX errors ## Common Mistakes and Fixes | Mistake | Fix | |---------|-----| | HTMX request returns nothing | Check hx-target element exists | | Page refresh instead of swap | Add hx-target, check hx-boost | | Form not submitting | Ensure button type="submit" | | Styles not updating | Run `bun run css` or check Tailwind config | | Template changes not showing | Run `templ generate` or check air is running | | "Not found" in mobile app | Check route is registered in app.go | ## Quick Reference ### Router ```go r.GET("/path", handler) r.POST("/path", handler) r.PUT("/path", handler) r.DELETE("/path", handler) r.Group("/prefix", func(r *router.Router) { ... }) r.Static("/static", "static") ``` ### Context ```go ctx.Param("id") // URL param ctx.Query("key") // Query string ctx.FormValue("name") // Form field ctx.IsHTMX() // Is HTMX request? ctx.Trigger("event") // Fire HX-Trigger ctx.Redirect("/path") // Redirect ``` ### HTMX Attributes ```html hx-get="/url" hx-post="/url" hx-put="/url" hx-delete="/url" hx-target="#id" hx-swap="innerHTML" hx-trigger="click" hx-indicator="#spin" hx-confirm="Sure?" hx-boost="true" hx-swap-oob="true" ``` ### WebSocket Attributes (HTMX 4) ```html hx-ws:connect="/path" hx-ws:send hx-vals='{"k":"v"}' hx-swap-oob="true" hx-swap-oob="afterbegin" hx-swap-oob="beforeend" ``` ### WebSocket Message Formats ```go // Server → Client (JSON envelope) type WSEnvelope struct { Channel string `json:"channel,omitempty"` // "ui" default Format string `json:"format,omitempty"` // "html" default Target string `json:"target,omitempty"` // CSS selector Swap string `json:"swap,omitempty"` // swap strategy Payload string `json:"payload"` // HTML content RequestID string `json:"request_id,omitempty"` } // Client → Server (from hx-ws:send) type WSRequest struct { Type string `json:"type"` // "request" RequestID string `json:"request_id"` // for response matching Event string `json:"event"` // "click", "submit" Values map[string]any `json:"values"` // form data / hx-vals } ``` ### WebSocket Broadcast ```go func Broadcast(html string) { env := WSEnvelope{Payload: html} data, _ := json.Marshal(env) for c := range clients { c.WriteMessage(websocket.TextMessage, data) } } ``` ### templ Syntax ```go templ Name(args) { } // Define component @Component() // Use component { variable } // Output value { children... } // Slot for children if cond { } else { } // Conditional for _, x := range xs { } // Loop class={ templ.KV("a", bool) } // Conditional class attr?={ bool } // Conditional attribute ```