zebflow 0.7.9

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
# Webhook → TSX Page

## What this builds

A server-rendered HTML page triggered by HTTP GET. The query result flows directly into the TSX template as `input`. Standard pattern for every data-driven page in Zebflow.

---

## Core Pattern

```
trigger.webhook → (optional query node) → web.response --template pages/foo.tsx
```

The upstream node's entire output becomes `input` inside the TSX template. `input.rows` for pg.query results, `input.data` or whatever shape the script returns.

---

## Pipelines

### Simple page — static data via script

```
| trigger.webhook --path /hello --method GET
| script -- "return { message: 'Hello World', ts: Date.now() }"
| web.response --template pages/hello.tsx
```

### Page with PostgreSQL list

```
| trigger.webhook --path /programmes --method GET
| pg.query --credential my-pg \
    -- "SELECT unit_id::text, code, title->>'id' as title, slug FROM academic.academic_unit WHERE unit_type = 'programme' AND is_active = true ORDER BY code"
| web.response --template pages/programmes.tsx
```

In `pages/programmes.tsx` — `input.rows` is the array of DB rows:

```tsx
const rows = input?.rows ?? [];
return (
  <ul>
    {rows.map(p => <li key={p.unit_id}>{p.title} ({p.code})</li>)}
  </ul>
);
```

### Detail page with path param — `:unit_id` → `$1`

```
| trigger.webhook --path /programmes/:unit_id --method GET
| pg.query --credential my-pg --params-path params.unit_id \
    -- "SELECT unit_id::text, code, title, description FROM academic.academic_unit WHERE unit_id = $1::uuid AND is_active = true"
| script -- "const r = input.rows?.[0]; if (!r) return { notfound: true }; return r"
| web.response --template pages/programme-detail.tsx --status 404
```

Wait — the status 404 should only apply when not found. Better pattern: two separate pipelines, or use a script to set the status conditionally. For the not-found case:

```
| script -- "const r = input.rows?.[0]; if (!r) return { notfound: true }; return r"
| web.response --template pages/not-found.tsx --status 404
```

In `pages/programme-detail.tsx`:

```tsx
export default function Page(input) {
  if (input?.notfound) return <Page><main><h1>Not Found</h1></main></Page>;
  return (
    <Page>
      <main>
        <h1>{input?.title?.id}</h1>
        <p>{input?.code}</p>
      </main>
    </Page>
  );
}
```

### Page with query string filter — `?faculty_id=uuid`

```
| trigger.webhook --path /programmes --method GET
| pg.query --credential my-pg --params-expr "[input.query.faculty_id ?? null]" \
    -- "SELECT unit_id::text, code, title->>'id' as title FROM academic.academic_unit WHERE unit_type = 'programme' AND ($1::uuid IS NULL OR parent_unit_id = $1::uuid) ORDER BY code"
| web.response --template pages/programmes.tsx
```

---

## Nodes Used

- `trigger.webhook` — GET endpoint; path params in `input.params.<name>`, query string in `input.query.<name>`
- `pg.query` — fetch data; `--params-path params.unit_id` binds `:unit_id` as `$1`
- `script` — 404 guard, data transform
- `web.response` — renders TSX template; upstream output = `input` in template; supports `--status`, `--set-cookie`, `--header`

---

## Templates Needed

Use `template_create kind=page name=<page-name>` then `template_write` to fill content.
Access upstream data via `input` (function parameter) — it's whatever the previous node returned.
`ctx` is the same object available as `globalThis.ctx` in both SSR and browser.