# 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) {
for _, item := range items {
{ item.Name }
}
}
// 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
Load Items
Delete
```
### 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
Home
About
```
### Loading Indicators
```html
Load
...
```
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() {
Tab 1
Tab 2
@TabOne()
}
```
### Modal Dialog
```go
templ ModalTrigger() {
Open Modal
}
templ Modal(title string) {
{ title }
{ children... }
Close
}
templ ConfirmModal() {
@Modal("Confirm Action") {
Are you sure?
Confirm
}
}
```
### Form Validation
```go
templ FormField(name, label, value, error string) {
{ label }
{ 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
}
}
```
### Sending Data to Server
Use `hx-ws:send` to send data from client to server:
```go
templ Counter() {
}
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") {
}
}
```
### 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
}
```
### WebSocket Notifications
```go
templ NotificationToast(message, msgType string) {
}
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) {
{ text }
}
```
### Pull to Refresh
```go
templ PullToRefresh(contentPath string) {
}
```
## 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
```