CLAUDE.md.tmpl

175 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 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
# <<.Name>>

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

## Commands

```bash
congo dev         # development server with hot reload (ENV=development, in-memory DB)
congo test        # run project tests
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)

## Application Entry Point

```go
router.Listen(
    router.WithLogger(),
    security.New(security.WithNonce(), security.WithHeaders()),
    application.New(views,
        application.WithController(controllers.Home()),
    ),
)
```

## 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)
    app.Handle("GET /todos", app.Serve("todos.html", nil))
    app.Handle("POST /todos", app.Method(c, "Create", nil))
    app.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)
- App embeds `*http.ServeMux` — routes via `app.Handle`, `app.HandleFunc`
- `cmp.Or()` for env var defaults

## Security

```go
// Password hashing
hash, _ := security.HashPassword(password)
ok := security.CheckPassword(hash, password)

// JWT sessions
token, _ := security.CreateToken(claims, secret, 30*24*time.Hour)
claims, _ := security.ValidateToken(token, secret)
security.SetSessionCookie(w, "session", token, 30*24*time.Hour)
security.ClearSessionCookie(w, "session")
```

## Application Options

```go
application.WithValue("key", value)         // template function returning value
application.WithHealthPath("/healthz")      // custom health endpoint (default: /health)
application.WithController(controllers.X()) // register controller
application.WithFunc("name", fn)            // register template function
```