claude-context.md
185 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# Congo Project
Built with the Congo framework — Go + HTMX + DaisyUI + React islands.
## Commands
```bash
congo dev # development server (ENV=development, in-memory DB)
congo build # production binary
congo launch # deploy to remote server
congo new <name> # add a new app to the project
congo connect # SSH into deployed server
congo destroy # tear down infrastructure
congo claude # AI-assisted development
congo status # check deployment health
congo logs # stream service logs
congo source # extract flat buildable copy of framework
```
## Architecture
MVC with HTMX-first server rendering. React islands for complex interactive UI.
```
controllers/ Route handlers + template methods
models/ Database models with auto-migration ORM
views/ Go HTML templates (HTMX + DaisyUI)
components/ React island components (optional)
internal/ Vendored Congo framework (modifiable)
```
## Controller Pattern
```go
// Factory function returns (name, controller).
func Todos() (string, *TodosController) {
return "todos", &TodosController{}
}
type TodosController struct {
application.BaseController // Always embed
}
func (c *TodosController) Setup(app *application.App) {
c.BaseController.Setup(app)
http.Handle("GET /todos", app.Serve("todos.html", nil))
http.Handle("POST /todos", app.Method(c, "Create", nil))
http.Handle("POST /todos/{id}/delete", app.Method(c, "Delete", nil))
}
// VALUE receiver — creates copy for request isolation.
func (c TodosController) Handle(r *http.Request) application.Controller {
c.Request = r
return &c
}
// Template methods (pointer receiver) — accessible as {{todos.All}}.
func (c *TodosController) All() []*models.Todo {
todos, _ := models.Todos.Search("ORDER BY CreatedAt DESC")
return todos
}
// POST handler — create.
func (c *TodosController) Create(w http.ResponseWriter, r *http.Request) {
todo := &models.Todo{Title: r.FormValue("title")}
if _, err := models.Todos.Insert(todo); err != nil {
c.RenderError(w, r, err)
return
}
c.Refresh(w, r) // reload current page
}
// POST handler — delete.
func (c *TodosController) Delete(w http.ResponseWriter, r *http.Request) {
if err := models.Todos.DeleteByID(r.PathValue("id")); err != nil {
c.RenderError(w, r, err)
return
}
c.Redirect(w, r, "/todos") // navigate to different URL
}
```
Register in `main.go`:
```go
application.WithController(controllers.Todos()),
```
## Model Pattern
```go
type Todo struct {
database.Model // ID (string UUID), CreatedAt, UpdatedAt
Title string
Done bool
}
var Todos = database.Manage(DB, new(Todo),
database.WithIndex[Todo]("Title"),
)
```
Database init in `models/db.go`:
```go
var DB = engines.NewAuto() // env-based: DB_URL+DB_TOKEN=remote, DB_PATH=local, neither=memory
```
Collection methods: `Get(id)`, `First(where, args...)`, `Search(where, args...)`, `All()`, `Insert(entity)`, `Update(entity)`, `Delete(entity)`, `DeleteByID(id)`, `Count(where, args...)`.
Query examples:
```go
models.Todos.Search("WHERE Done = ? ORDER BY CreatedAt DESC LIMIT 10", false)
models.Todos.First("WHERE Title = ?", "Buy milk")
models.Todos.Count("WHERE Done = ?", true)
```
## View + HTMX Pattern
```html
{{template "main.html" .}}
{{define "title"}}Todos{{end}}
{{define "content"}}
<div class="container mx-auto p-8">
{{range $todo := todos.All}}
<div class="card bg-base-100 mb-2 p-4 flex justify-between items-center">
<span>{{$todo.Title}}</span>
<button hx-post="/todos/{{$todo.ID}}/delete" hx-target="body" class="btn btn-sm btn-error">
Delete
</button>
</div>
{{end}}
<form hx-post="/todos" hx-target="body" class="flex gap-2 mt-4">
<input name="title" class="input input-bordered flex-1" required />
<button class="btn btn-primary">Add</button>
</form>
</div>
{{end}}
```
## Key Conventions
- **IDs are ALWAYS strings** (UUIDs). Never convert to int.
- **SQL uses PascalCase**: `WHERE UserID = ?` not `WHERE user_id = ?`
- **Templates by filename only**: `{{template "nav.html" .}}` not `"partials/nav.html"`
- **Render partial**: `c.Render(w, r, "partial.html", data)` in POST handlers
- **Error handling**: `c.RenderError(w, r, err)` returns 200 OK with error HTML for HTMX
- **Redirect**: `c.Redirect(w, r, "/path")` — sends HX-Location header for HTMX, 303 for regular requests
- **Refresh**: `c.Refresh(w, r)` — sends HX-Refresh header for HTMX, 303 redirect-to-self for regular requests
- **CSRF**: HTMX + SameSite=Lax cookies provides CSRF protection. No tokens needed.
- **PathValue**: in handlers `r.PathValue("id")`, in template methods `c.PathValue("id")`
## Application Options
```go
application.WithValue("key", value) // template function returning value
application.WithMiddleware(mw) // add middleware to handler chain
application.WithHealthPath("/healthz") // custom health endpoint (default: /health)
application.WithController(controllers.X()) // register controller
application.WithFunc("name", fn) // register template function
```
## Go Style
- Package-level vars as runtime state (no config structs)
- `cmp.Or()` for env var defaults (Go 1.22+)
- No custom ServeMux — use `http.DefaultServeMux`
- No single-line wrapper methods around stdlib calls
- Functions exist for reuse or naming — inline single-use logic
## Frontend Islands (when present)
React components in `components/`, mounted via `{{render "ComponentName" props}}` in templates. Auto-mounted on page load and HTMX swaps. HMR in development mode.
```html
{{render "Counter" home.CounterProps}}
```
Controller provides props:
```go
func (c *HomeController) CounterProps() map[string]any {
return map[string]any{"initial": 0, "label": "Clicks"}
}
```