# Build a page with components

Components are Cicadel's replacement for Django templates. This walkthrough builds
a card component and a list page, then wires up the view, URL and tests.

Every snippet below is real code. `TestComponentAuthoringGuide` copies each file
into a generated project, then compiles and renders it.

See also:

- [Component UI](https://cicadel.fauzaanu.com/en/dev/topics/component-ui/index.md): the full API contract.
- [Tailwind CSS](https://cicadel.fauzaanu.com/en/dev/howto/tailwind/index.md): styling.
- [`$cicadel-components`](https://cicadel.fauzaanu.com/en/dev/sources/skills/cicadel-components/SKILL.md): the reusable agent skill.

## Ask your agent

```text
Read https://cicadel.fauzaanu.com/en/dev/howto/component-authoring/index.md.
Add [screen] to my application using typed Cicadel components.
Inspect the current app structure, compose the page from explicit props and slots,
and connect its handler and route. Build the UI and test its rendered output.
```

## Translate what you know

Think "props down, markup composed, HTML returned." Cicadel borrows React's
composition model, not its JavaScript runtime.

| React habit | Cicadel equivalent |
| --- | --- |
| Function component with typed props | Private `.templ` function; public `ui.Definition[Props, Slots]` contract |
| `<Card title="Hello">…</Card>` | `@Card.Call(CardProps{Title: "Hello"}, CardSlots{Children: ...})` |
| `children` / a ReactNode prop | `ui.Content` fields named `Children`, `Footer`, etc. |
| Fragment `<>…</>` | Private markup helper via `ui.Fragment(helper(...))`, or `ui.Group(...)` |
| `{title}` | `{ p.Title }` (text escapes automatically) |
| `{items.map(...)}` | A multiline Go `for` block |
| Conditional JSX | A multiline Go `if` or `switch` block |
| Default/destructured props | Go zero values plus a pure `Defaults` function |
| `className`, `htmlFor` | HTML `class`, `for` |
| Boolean HTML prop | `disabled?={ p.Disabled }` |
| Utility classes | Tailwind v4, complete class strings in source |
| Context provider / hook state | Explicit typed inputs; handlers load data and process POSTs |
| `onClick` / hydration | Not part of this server engine; use links/forms for server interactions |
| `key` | No reconciliation runtime, so no key prop; keep DOM IDs unique for labels |

Do not add JSX, hooks, npm or a browser CDN script to write components. Tailwind
runs from a pinned standalone build tool.

## Set up the project

1. Generate a project with the module path `example.com/componentguide`.
2. Run `go mod tidy`.
3. Install the CSS compiler once with `go tool cicadel ui css install`.

You add the files below to an ordinary local Go app. You do not need a new module.

## 1. Define the contract

The contract declares the props, slots, defaults and validation.

### apps/dashboard/card.go

<!-- file: apps/dashboard/card.go -->
```go
package dashboard

import (
    "fmt"
    "github.com/fauzaanu/cicadel/ui"
)

type CardProps struct {
    Title string
    Tone string
}
type CardSlots struct { Children, Footer ui.Content }

var Card = ui.Definition[CardProps, CardSlots]{
    Name: "dashboard:card",
    Source: "apps/dashboard/card.templ",
    Defaults: func(p CardProps) CardProps {
        if p.Tone == "" { p.Tone = "neutral" }
        return p
    },
    Validate: func(p CardProps, s CardSlots) error {
        if err := ui.Required("Title", p.Title); err != nil { return err }
        if s.Children.Empty() { return fmt.Errorf("Children slot is required") }
        if p.Tone != "neutral" && p.Tone != "success" {
            return fmt.Errorf("unknown Tone %q", p.Tone)
        }
        return nil
    },
    Body: card,
}

func cardClasses(tone string) string {
    if tone == "success" { return "rounded-xl border border-emerald-300 bg-emerald-50 p-6" }
    return "rounded-xl border border-slate-200 bg-white p-6"
}
```

The rules this contract sets:

- An empty `Tone` defaults to `neutral`.
- An empty `Title` or a missing `Children` slot is an error.
- `Footer` is optional.

Zero and `false` are not always "missing". Use pointer props only when an omitted
value must differ from a supplied zero or `false`.

> [!WARNING]
> `Defaults` must not mutate objects referenced by caller-owned maps, slices or pointers.

## 2. Write the markup

### apps/dashboard/card.templ

<!-- file: apps/dashboard/card.templ -->
```templ
package dashboard

templ card(p CardProps, s CardSlots) {
    <section class={ cardClasses(p.Tone) }>
        <h2 class="text-xl font-semibold text-slate-900">{ p.Title }</h2>
        <div class="mt-3 text-slate-700">@s.Children</div>
        if !s.Footer.Empty() {
            <footer class="mt-4 text-sm text-slate-500">@s.Footer</footer>
        }
    </section>
}
```

Two kinds of insertion, on purpose:

| Syntax | Inserts |
| --- | --- |
| `{ p.Title }` | Escaped text. |
| `@s.Children` | A typed HTML fragment. |

Write complete utility class names in Go variants, as `cardClasses` does. Tailwind
cannot evaluate `"bg-" + color + "-50"`.

## 3. Compose the page

### apps/dashboard/page.templ

<!-- file: apps/dashboard/page.templ -->
```templ
package dashboard

import "github.com/fauzaanu/cicadel/ui"
import "github.com/fauzaanu/cicadel/ui/layout"

templ itemFooter() {
    <a class="underline hover:text-indigo-600" href="/">Back to home</a>
}

templ dashboardBody(rows []Item) {
    <div class="mx-auto max-w-3xl space-y-6 px-6 py-12">
        <h1 class="text-3xl font-bold">Dashboard</h1>
        if len(rows) == 0 {
            <p>No items yet.</p>
        }
        for _, row := range rows {
            @Card.Call(CardProps{Title: row.Title, Tone: row.Tone}, CardSlots{
                Children: ui.Text(row.Description),
                Footer: ui.Fragment(itemFooter()),
            })
        }
    </div>
}

templ dashboardPage(rows []Item, stylesheet string) {
    @layout.Page.Call(layout.Props{Title: "Dashboard", Stylesheet: stylesheet},
        layout.Slots{Children: ui.Fragment(dashboardBody(rows))})
}
```

- Private helpers such as `itemFooter` need no public definition. Add one only for
  a reusable, validated contract.
- `ui.Text` wraps request text and escapes it.
- `ui.Fragment` adapts compiled markup. Never pass it strings.
- `ui.Group` combines fragments that are already typed.

## 4. Add the view and URL

### apps/dashboard/app.go

<!-- file: apps/dashboard/app.go -->
```go
package dashboard

import (
    "context"
    "log/slog"
    "net/http"
    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/response"
    "github.com/fauzaanu/cicadel/static"
    "github.com/fauzaanu/cicadel/ui"
    "github.com/fauzaanu/cicadel/urls"
)

type Item struct { Title, Description, Tone string }
type LoadItems func(context.Context) ([]Item, error)

func New(load LoadItems) cicadel.App {
    return cicadel.App{
        Name: "dashboard",
        Components: []ui.Export{Card.Export(1)},
        Routes: []urls.Route{urls.Path("GET", "/dashboard", "dashboard:index",
            func(w http.ResponseWriter, r *http.Request) {
                rows, err := load(r.Context())
                if err != nil { response.Error(w, 500); return }
                stylesheet, err := static.URL(r.Context(), "project", "app.css")
                if err != nil { response.Error(w, 500); return }
                err = (ui.Renderer{}).HTML(r.Context(), w, 200, ui.Fragment(dashboardPage(rows, stylesheet)))
                if err != nil {
                    slog.ErrorContext(r.Context(), "render dashboard", "error", err)
                    if !ui.Committed(err) { response.Error(w, 500) }
                }
            }),
        },
    }
}
```

- The loader is explicit. It can query through your application's database.
- The component never discovers a request-wide map.
- `static.URL` resolves the project's compiled stylesheet to its content-hashed URL.
  The handler passes it in as a prop. See [static files](https://cicadel.fauzaanu.com/en/dev/howto/static-files/index.md).
- Keep permissions in the handler and its query scope.

### Register the app: project/apps.go

For the runnable demo, replace the starter's empty app list with this loader:

<!-- file: project/apps.go -->
```go
package project

import (
    "context"
    "example.com/componentguide/apps/dashboard"
    "github.com/fauzaanu/cicadel"
    "gorm.io/gorm"
)

func InstalledApps(_ *gorm.DB) ([]cicadel.App, error) {
    return []cicadel.App{dashboard.New(func(ctx context.Context) ([]dashboard.Item, error) {
        return []dashboard.Item{{Title: "Ready", Description: "Typed props and slots work.", Tone: "success"}}, ctx.Err()
    })}, nil
}
```

In your own project, use your module's import path.

A direct call to `Card` uses that definition. For an overridable library, resolve it
through `ComponentRegistry` and inject the result into the page factory. See
[the override recipe](https://cicadel.fauzaanu.com/en/dev/sources/skills/cicadel-components/references/patterns.md#libraries-and-project-overrides).

## 5. Test the behavior

### apps/dashboard/app_test.go

<!-- file: apps/dashboard/app_test.go -->
```go
package dashboard

import (
    "context"
    "strings"
    "testing"
    "testing/fstest"
    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/apptest"
    "github.com/fauzaanu/cicadel/ui"
)

func TestDashboard(t *testing.T) {
    attack := `<script>alert("x")</script>`
    project := cicadel.App{Name: "project", Static: fstest.MapFS{"app.css": {Data: []byte("body{}")}}}
    app := apptest.New(t, apptest.Settings(), []cicadel.App{project,
        New(func(context.Context) ([]Item, error) {
            return []Item{{Title: attack, Description: attack}}, nil
        }),
    })
    stylesheet, err := app.Static().URL("project", "app.css")
    if err != nil { t.Fatal(err) }
    res := apptest.NewClient(app).Get(t, "/dashboard").Status(t, 200)
    res.Contains(t, `rel="stylesheet"`).Contains(t, `href="`+stylesheet+`"`)
    res.Contains(t, "Back to home").Contains(t, "border-slate-200")
    if strings.Contains(string(res.Body), "<script>") { t.Fatal("unescaped input") }
}

func TestCardContractAndEmptyList(t *testing.T) {
    renderer := ui.Renderer{}
    if _, err := renderer.Render(t.Context(), Card.Call(CardProps{Title: "x"}, CardSlots{})); err == nil {
        t.Fatal("missing children accepted")
    }
    if _, err := renderer.Render(t.Context(), Card.Call(CardProps{Title: "x", Tone: "invalid"}, CardSlots{Children: ui.Text("x")})); err == nil {
        t.Fatal("invalid tone accepted")
    }
    out, err := renderer.Render(t.Context(), ui.Fragment(dashboardPage(nil, "")))
    if err != nil || !strings.Contains(string(out), "No items yet.") { t.Fatalf("%s: %v", out, err) }
}
```

These tests check behavior, not a full HTML snapshot. Add assertions for your own
fields, labels, error relationships, links and authorization rules.

## The edit loop

```sh
go tool cicadel ui build
go tool cicadel ui check --json
go test -race ./...
go run manage.go check
```

Then restart `go run manage.go runserver` to load the rebuilt embedded markup and CSS.

- `ui check` reports missing or stale artifacts. `ui build` fixes them.
- Commit `.templ` files, ordinary Go, CSS input and config, generated Go and generated CSS together.
- Do not hand-edit generated output.
- The starter's CSS is already compiled. The first run does not download a compiler or need Node.
  After UI changes, install the tool once and rebuild.
- Class sources from installed libraries go in the `packages` list of `ui.css.json`.

## Common mistakes

- Write multiline `if` and `for` blocks, not JavaScript ternaries or inline JSX blocks.
- Use `class`, not `className`. Use complete utility names, not dynamic name fragments.
- `ui.Merge` merges tokens, not Tailwind conflict groups. With `p-2 p-6`, the last
  one is not promised to win. Pick a single spacing variant instead.
- Pass slot values explicitly. `{ children... }` is intentionally unsupported.
- Keep CSS out of markup. Inline styles, scripts, event attributes and active SVG
  are not in the initial component contract. Do not work around that with raw HTML.
- Keep CSRF and accessible errors by using `ui/form` with explicit session tokens.
- Fix failures in the authored source. JSON diagnostics point to source components
  and keep generated Go positions. Never fix a type error in `*_templ.go`.

For targeted fixes to forms, defaults and forwarding, see the
[skill's pattern reference](https://cicadel.fauzaanu.com/en/dev/sources/skills/cicadel-components/references/patterns.md).

## Give the skill to your coding agent

The portable skill lives at `skills/cicadel-components/`, with its patterns and
debugging reference. The repository's `AGENTS.md` points agents to it for UI tasks.

With Codex:

1. Copy the directory into `${CODEX_HOME:-$HOME/.codex}/skills/`, or use your skill installer.
2. Keep the reference directory next to `SKILL.md`.
3. Invoke `$cicadel-components`.

The skill needs no browser tools or runtime plugin. Other coding agents can read the
same Markdown directly. If an agent loaded its skill catalog before you installed
the skill, refresh or reopen the session.
