CLAUDE.md.tmpl

148 lines
1 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
# <<.Name>>

Built with the Congo framework (Go + HTMX + DaisyUI).

## 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
```

## Architecture

MVC pattern with HTMX-first server rendering.

- `<<.Dir>>/controllers/` — route handlers + template methods
- `<<.Dir>>/models/` — database models with auto-migration ORM
- `<<.Dir>>/views/` — Go HTML templates with DaisyUI
<<- if .WithFrontend>>
- `<<.Dir>>/components/` — React island components
<<- end>>
- `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
}

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 — 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 `<<.Dir>>/main.go`:
```go
application.WithController(controllers.Todos()),
```

## Model Pattern

```go
// <<.Dir>>/models/todo.go
type Todo struct {
    database.Model  // ID (string UUID), CreatedAt, UpdatedAt
    Title    string
    Done     bool
}
```

Register collection in `<<.Dir>>/models/db.go`:
```go
var Todos = database.Manage(DB, new(Todo))
```

Tables auto-create on startup. New fields auto-migrate. Collection methods: `Get(id)`, `First(where, args...)`, `Search(where, args...)`, `All()`, `Insert(entity)`, `Update(entity)`, `Delete(entity)`, `DeleteByID(id)`, `Count(where, args...)`.

## 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" placeholder="New todo..." required />
        <button class="btn btn-primary">Add</button>
    </form>
</div>
{{end}}
```

## Key Conventions

- IDs are ALWAYS strings (UUIDs), never integers
- SQL columns use PascalCase: `WHERE UserID = ?`
- Templates by filename only: `{{template "nav.html" .}}` not `"partials/nav.html"`
- `c.Render(w, r, "template.html", data)` — render a partial in POST handlers
- `c.RenderError(w, r, err)` — returns 200 with error HTML for HTMX
- `c.Redirect(w, r, "/path")` — sends HX-Location header for HTMX, 303 for regular requests
- `c.Refresh(w, r)` — sends HX-Refresh header for HTMX, 303 redirect-to-self for regular requests
- `r.PathValue("id")` in handlers, `c.PathValue("id")` in template methods
- HTMX + SameSite=Lax cookies = CSRF protection (no tokens needed)
- No custom ServeMux — all routes on `http.DefaultServeMux`
- `cmp.Or()` for env var defaults

## Application Options

```go
application.WithValue("key", value)         // template function returning value
application.WithMiddleware(mw)              // add middleware
application.WithHealthPath("/healthz")      // custom health endpoint (default: /health)
application.WithController(controllers.X()) // register controller
```