# Build web apps with GizWeb

This guide shows how to build a server-rendered web app with GizWeb. In about 20 minutes
you model your data, add an action and two pages, keep the notes in a database, and test the app.

- The guide is for developers who know Next.js, Remix or React.
- The tutorial builds one app step by step. The how-to guides each do one task. The
  explanation compares the model with Next.js.
- Each example ran on the current toolchain. Each response is the real response, from `curl`
  or from a browser. Long output is trimmed (`…`).
- A section with a status badge describes GizWeb (`gizweb`). Every other section runs on
  the SDK modules of the released toolchain.
- To install the toolchain, refer to the [installation guide](../start.md). For the
  `giz` command, refer to the [command line guide](command-line.md).

## What GizWeb is

GizWeb is a set of SDK modules. The compiler contains the SDK, so a project needs no other
installation. You import the modules with a `gizlang:` name:

| Module | Use it for |
| --- | --- |
| `gizlang:http` | The HTTP server (`serve`), `Request`, `Response`, headers, streamed replies; routes (`router`, `Route`, `resolve`, `href`), redirects, cookies, static files, URL encoding, gzip |
| `gizlang:html` | The `Html` type that markup compiles to, `each`, `when`, `Shadow` (declarative shadow DOM) |
| `gizweb` | GizWeb: pages and the app shell, routes with a context (`Web.route`, `Web.path`, `Web.typed`, `Web.before`, `Web.limited`, `Web.serve`), static pages (`Web.prerender`), forms, actions, live views, content and data views |
| `gizdb:model` | The record model: annotations, `Shape`, input checks, `Rejection`, DDL and JSON ([Model your data](model.md)) |
| `gizdb:store` | The app's database: `Engine`, `Database` |
| `gizlang:sqlite` | An embedded SQLite database |
| `gizlang:security` | Signed session tokens |
| `gizlang:tls` | HTTPS with your certificate |
| `gizlang:test` | Assertions for `X.test.giz` files |

Markup in `.giz` files has the same syntax as TSX. Its descriptive name is GSX. There is no
other file suffix and no other parser.

- A lowercase tag writes HTML.
- A capitalized tag calls a typed component function.
- `{value}` writes escaped text. Strings, integers, floats and `Html` values can be children.
- A markup expression has the type `Html`. Its bytes are in `.markup`.

## Tutorial: build a small web app

:::status{of="gizweb"}
:::

You build "Notes": a list of notes, one page for each note, and a form that adds a note. At the end,
the notes are in a GizDB folder and stay after a restart. One record type gives the table, the form,
the checks, the JSON API and the agent tool.

The finished app is `examples/web/notes`. The same app on
`gizlang:http` alone, without a model, is `examples/web/http`.

### Before you start

- Install the toolchain. `giz` must be on your `PATH`.
- Make an empty folder, and in it a folder `notes`. Run all commands from the empty folder.
- The examples listen on port 61100 of 127.0.0.1. If that port is in use, change the number in
  `notes/main.giz`.

### Step 1: write the model

A model is a record type with annotations. Make `notes/model.giz`:

```gizlang file="examples/web/notes/model.giz"
import {Table, Key, Label, Min, Max, Trim, Multiline, Submit} from "gizdb:model";

/** A saved note. */
@Table("notes")
export type Note = { @Key id: string; title: string; body: string; };

/** The form that adds a note. */
@Submit("Add note")
export type AddNote = {
    @Label("Title") @Min(1) @Max(80) @Trim title: string;
    @Label("Text") @Max(2000) @Multiline body: string;
};
```

- `Note` is what the database stores: the table `notes`, with `id` as its key.
- `AddNote` is what the form sends. Its annotations are the label, the limits and the trim of each
  field. They make the HTML form, the server checks and the JSON schema. The checks run on the
  server for every caller: a browser, an API client and an agent.

### Step 2: write the action

An action is an ordinary function, listed once in a registry. Make `notes/actions.giz`:

```gizlang file="examples/web/notes/actions.giz"
import * as Model from "gizdb:model";
import {Note, AddNote} from "./model.giz";
import {Data} from "gizweb";

/** What an action runs with: the database and a fresh id. */
export type Session = { db: Data.Db; fresh: string; };

function addNote(s: Session, input: AddNote): Result<Note, Model.Rejection> {
    const note: Note = { id: s.fresh, title: input.title, body: input.body };
    const shape = Model.shape<Note>();
    try Data.write(s.db, [try Data.insert(shape, note)], "");
    return Result.ok(note);
}
/** The actions: people (a form), pages and agents (a tool) call them by name. */
export function actions(): Vec<Model.Action<Session>> {
    return [Model.action<Session, AddNote, Note>("addNote", "Add a note.", addNote)];
}
```

- The function gets what it runs with (`Session`) and the checked input. It gives a `Result`.
- `Model.action` puts it in the registry with its name and its doc. The input type gives the form, the
  checks and the tool schema, read from the type at run time.
- `Model.shape<Note>()` reads the Shape of `Note` from its declaration. `Data.insert` makes a typed write from it. `Data.write` runs it in one transaction.
- `try` returns the error of a failed step at once.

### Step 3: write the pages and serve

The app is in `notes/app.giz`, and `notes/main.giz` starts it. Make `notes/app.giz`:

```gizlang file="examples/web/notes/app.giz"
import * as Web from "gizweb";
import * as Model from "gizdb:model";
import * as Store from "gizdb:store";
import {System} from "gizlang:system";
import * as Http from "gizlang:http";
import * as Text from "gizlang:text";
import {Html, each} from "gizlang:html";
import {Data, Forms} from "gizweb";
import {Note} from "./model.giz";
import {Session, actions} from "./actions.giz";

/** What each serving thread shares. */
export type Site = { db: Data.Db; note: Model.Shape; shell: Web.Shell; add: Forms.Form; sys: System; };
type Handle = Web.Handle<Site>;

/** Each route once: its method, pattern and handler. Links come from the same declaration. */
type NotePath = { id: string; };
function notePath(): Web.Path<NotePath> { return Web.path<NotePath>("GET", "/notes/{id}"); }
function noteRoute(): Http.Route<Handle> {
    return Web.typed(notePath(), (site: Site, request: Http.Request, p: NotePath): Http.Response => {
        match (Data.one(site.db, site.note, "id = $1", [p.id], Model.rowReader<Note>(site.note), "No such note.")) {
            Result.ok(n) => { return Web.page(request, site.shell, n.title, <main><h1>{n.title}</h1><p>{n.body}</p></main>.markup, "no-cache"); }
            Result.err(r) => { return Web.respond(request, Result.err(r)); }
        }
    });
}
function homeRoute(): Http.Route<Handle> {
    return Web.route("GET", "/", (site: Site, request: Http.Request, p: Http.Params): Http.Response => {
        const notes = Data.find(site.db, site.note, "true", [], Model.rowReader<Note>(site.note))!;
        let form = b"";
        site.add.write(form, Model.noFields().with(Forms.GO, "/notes/:id"));
        const link = (n: Note): string => { return Web.link(notePath(), NotePath(n.id))!; };
        const body = <main><h1>Notes</h1><ul>{each(notes, (n: Note) => <li><a href={link(n)}>{n.title}</a></li>)}</ul>{Html(form)}</main>;
        return Web.page(request, site.shell, "Notes", body.markup, "no-cache");
    });
}

/** The engine of the GizDB folder `path`, with the tables of the models. */
export function open(sys: System, path: string): Store.Engine {
    const engine = Web.store(sys, path)!.engine()!;
    Data.install(Data.of (engine, "notes")!, [Model.shape<Note>()])!;
    return engine;
}
export function shell(): Web.Shell { return Web.shell("en", Model.handlers(actions()), Web.uiStyle()); }
/** The handler of one serving thread: its connection to the one engine of the process. */
export function app(own: System, engine: Store.Engine): (request: Http.Request) => Http.Response {
    const list = actions();
    const handlers = Model.handlers(list);
    const site: Site = {
        db: Data.of (engine, "notes")!,
        note: Model.shape<Note>(),
        shell: shell(),
        add: Forms.compile(handlers[0], ""),
        sys: own
    };
    const session = (s: Site, r: Http.Request): Session => {
        return { db: s.db, fresh: Text.hexEncode(s.sys.randomBytes(4)!) };
    };
    let routes = [homeRoute(), noteRoute()];
    for (const r of Web.actions(handlers, session, (
        s: Session, id: string, bare: bool, fields: Model.Fields
    ): Model.Outcome => {
        return Model.dispatch(list, s, id, bare, fields);
    })) {
        routes.push(r);
    }
    const router = Http.router(routes)!;
    return (request: Http.Request): Http.Response => { return Web.serve(router, site, request); };
}
```

- A route is declared once, as a function: method, pattern and handler. `noteRoute().href(params)`
  makes the link from the same declaration, so a changed pattern changes each link.
- `Data.find` and `Data.one` read records of a Shape with a filter. The values are bound (`$1`),
  never put into the SQL text. `Model.rowReader<Note>` reads a row into a `Note`.
- `Forms.compile` makes the form of an action once. The hidden field `_go` (`Forms.GO`) sends the
  browser to the new note after a valid post (`:id` is the `id` of the action's answer).
- `Web.actions` adds the routes of the actions: a form posts to `/_a/ID`, an agent calls the name,
  and the tools are listed at `/_tools`. `Web.page` writes a page with the shell, the style and the
  ETag.
- `open` makes the tables at start. The process holds one engine of the GizDB folder. Each serving
  thread makes its own connection to it (`Data.of`).

Make `notes/main.giz`:

```gizlang file="examples/web/notes/main.giz"
// The notes app on GizWeb (app.giz): one model, one action, two pages. Run this file with gizstack.
import * as Web from "gizweb";
import {System} from "gizlang:system";
import * as Http from "gizlang:http";
import {open, shell, app} from "./app.giz";

function main(sys: System): i64 {
    const engine = open(sys, "data");
    return Web.listen(sys, 61100, shell(), Web.serving(), (own: System): (request: Http.Request) => Http.Response => {
        return app(own, engine);
    });
}
```

Run the app, and use it:

```sh
giz notes/main.giz
```

```text
listening on http://127.0.0.1:61100
```

In another terminal, post the form the way a browser does. The hidden field `_go` is in the form:

```sh
curl -i -H 'Referer: http://127.0.0.1:61100/' --data-urlencode 'title=Plan trip' \
  --data-urlencode 'body=Book trains & hotel' --data-urlencode '_go=/notes/:id' http://127.0.0.1:61100/_a/addNote
```

```text
HTTP/1.1 303 See Other
location: /notes/ccc1b3b1
…
```

The id is random, so yours is different. An empty title gets `303` back to the page, and the page shows
the error next to the field. The same action answers a JSON client with the same check:

```sh
curl -H 'content-type: application/json' -d '{"title":"","body":"x"}' http://127.0.0.1:61100/_a/addNote
curl -H 'content-type: application/json' -d '{"title":"From JSON","body":"b"}' http://127.0.0.1:61100/_a/addNote
```

```text
{"ok":false,"status":422,"field":"title","message":"Title: Enter a value.","errors":[{"field":"title","message":"Title: Enter a value."}]}
{"ok":true,"value":{"id":"ea053105","title":"From JSON","body":"b"}}
```

An agent finds the action as a tool, with the schema that the annotations gave:

```sh
curl http://127.0.0.1:61100/_tools
```

```text
[{"name":"addNote","id":"addNote.6f1a…","description":"Add a note.","input_schema":{"type":"object","properties":{"title":{"type":"string","description":"Title","minLength":1,"maxLength":80},"body":{"type":"string","description":"Text","maxLength":2000}},"required":["title","body"]}}]
```

The list and a note are pages of server HTML. The text that the user typed comes back as escaped text:

```sh
curl http://127.0.0.1:61100/
curl http://127.0.0.1:61100/notes/ccc1b3b1
```

```text
<!doctype html>…<main><h1>Notes</h1><ul><li><a href="/notes/ccc1b3b1">Plan trip</a></li><li><a href="/notes/ea053105">From JSON</a></li></ul><form method="post" action="/_a/addNote" …>…</form></main></body></html>
<!doctype html>…<main><h1>Plan trip</h1><p>Book trains &amp; hotel</p></main></body></html>
```

Stop the server with Ctrl+C and start it again. The notes are still there, because they are in the folder `data`.

The same action answers the form, a JSON client and an agent. The checks are the same for all three.

### Step 4: test the app

A pure test needs no server and no database. Make `notes/main.test.giz`:

```gizlang file="examples/web/notes/main.test.giz"
import * as Model from "gizdb:model";
import * as Test from "gizlang:test";
import {AddNote} from "./model.giz";

function testATitleIsRequired(): void {
    const fields = Model.formFields("title=+&body=x")!;
    match (Model.read(Model.shape<AddNote>(), fields)) {
        Result.ok(row) => { Test.check(false); }
        Result.err(r) => { Test.equal(r.status, 422); Test.includes(r.message, "Title"); }
    }
}
function testATitleIsTrimmed(): void {
    const row = Model.read(Model.shape<AddNote>(), Model.formFields("title=+Milk+&body=x")!)!;
    Test.equal(Model.valueOf<AddNote>(Model.shape<AddNote>(), row).title, "Milk");
}
```

```sh
giz test notes/main.test.giz
```

```text
0 cached, 2 run, 0 failed
```

A test that needs the database runs the whole handler against a GizDB folder in a temporary
directory. Make `notes/journey.giz`:

```gizlang file="examples/web/notes/journey.giz"
import {System} from "gizlang:system";
import * as Http from "gizlang:http";
import * as Test from "gizlang:test";
import {open, app} from "./app.giz";

function post(path: string, form: string): Http.Request {
    return Http.Request("POST", path, [Http.Header("content-type", "application/x-www-form-urlencoded")], utf8(form));
}
function main(sys: System): void {
    const dir = sys.temporaryDir()!;
    const handle = app(sys, open(sys, `${dir.temporaryPath()!}/data`));
    const added = handle(post("/_a/addNote", "title=Milk&body=2+liters&_go=%2Fnotes%2F%3Aid"));
    Test.equal(added.status, 303);
    const location = Http.header(added.headers, "location") ?? "";
    Test.check(location.startsWith("/notes/"));
    Test.includes(string(handle(Http.Request("GET", location, [], b"")).body), "<h1>Milk</h1>");
    Test.includes(string(handle(Http.Request("GET", "/", [], b"")).body), "Milk");
    Test.equal(handle(Http.Request("GET", "/notes/none", [], b"")).status, 404);
    const refused = handle(post("/_a/addNote", "title=+&body=x"));
    Test.equal(refused.status, 422);
    Test.includes(string(refused.body), "Title");
    Test.includes(string(handle(Http.Request("GET", "/", [], b"")).body), "Milk");
    dir.close();
    print("journey ok");
}
```

```sh
giz notes/journey.giz
```

```text
journey ok
```

### Step 5: use the app in a browser

Open <http://127.0.0.1:61100/>. Type a title and a text, and click **Add note**. The browser goes to
the new note. Go back to the list: it shows the note. The form works with no JavaScript.

You now have a server-rendered app with routes, a form, validation, a JSON API and a database. The
next sections each do one task. [Choose how to reach the data](model.md#choose-how-to-reach-the-data) shows
the other levels below the model.

## How-to guides

### Declare routes and read parameters

A route is `Http.Route(METHOD, PATTERN, TARGET)`. `TARGET` is any value. Usually it is a union
of endpoints.

| Pattern part | Meaning |
| --- | --- |
| `/users` | A literal segment |
| `/users/{id}` | `{id}` captures one segment |
| `/files/*path` | `*path` captures the rest of the path, with its slashes |

- A literal segment has priority over a capture. A capture has priority over a rest capture.
  The order of the declarations has no effect.
- Two equal routes are refused when you call `Http.router`.
- Each captured value is decoded one time. A bad escape such as `%ZZ` gets `400`.
- `found.params.get(name)` gives the text. `found.params.integer(name)` gives an `Option<i64>`.
- `route.href(Http.Params(names, values))` makes a URL from a declaration. It escapes
  each segment. A missing parameter is an error.
- `Http.query(request)` decodes the query one time to `Params`. `get(name)` gives the first value,
  or `""`. A malformed query is an error with status 400 ([Serve HTTPS](#serve-https) has an example).

The how-to examples are in a folder `howto`, next to `notes`. Run them from the same folder as
the tutorial.

`howto/routes.giz`:

```gizlang
import {System} from "gizlang:system";
import {Result} from "gizlang:core";
import * as Http from "gizlang:http";

type Endpoint = | { kind: "readme"; } | { kind: "file"; } | { kind: "user"; };
function fileRoute(): Http.Route<Endpoint> { return Http.Route("GET", "/files/*path", Endpoint.file()); }
function main(sys: System): i64 {
    const routes = Http.router([
        Http.Route("GET", "/files/readme", Endpoint.readme()),   // a literal segment wins over a capture
        fileRoute(),                                             // *path captures the rest of the path
        Http.Route("GET", "/users/{id}", Endpoint.user()),       // {id} captures one segment
    ])!;
    print(fileRoute().href(Http.Params(["path"], ["docs/a b.txt"]))!);
    const listener = sys.listen("127.0.0.1", 61102)!;
    Http.serve(listener, (request: Http.Request): Http.Response => {
        match (routes.resolve(request)) {
            Result.err(response) => { return response; }   // 404, 405 with Allow, or 400 for a bad escape
            Result.ok(found) => {
                match (found.target) {
                    Endpoint.readme() => { return Http.text(200, "the readme"); }
                    Endpoint.file() => { return Http.text(200, "file " + found.params.get("path")); }
                    Endpoint.user() => { return Http.text(200, "user " + found.params.get("id")); }
                }
            }
        }
    })!;
    return 0;
}
```

```sh
giz howto/routes.giz
```

It prints the URL that `href` made, and then serves requests:

```text
/files/docs/a%20b.txt
```

```sh
curl http://127.0.0.1:61102/files/readme
curl http://127.0.0.1:61102/files/docs/a%20b.txt
curl http://127.0.0.1:61102/users/kim%2Fx
curl -i http://127.0.0.1:61102/users/%ZZ
curl -i -X DELETE http://127.0.0.1:61102/users/1
```

```text
the readme
file docs/a b.txt
user kim/x

HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
date: Wed, 14 Oct 2026 02:01:14 GMT
content-length: 28

Invalid path escape or UTF-8

HTTP/1.1 405 Method Not Allowed
content-type: text/plain; charset=utf-8
allow: GET, HEAD
date: Wed, 14 Oct 2026 02:01:14 GMT
content-length: 18

method not allowed
```

`%2F` in `{id}` stays in the one segment: the value is `kim/x`.

Parameters are text. Convert them with `params.integer` or your own check. With GizWeb, a route
target is a typed record, so `{id}` is an `i64` that the compiler checks.

### Name the parameters of a route in a record

A parameter named in a pattern is text that a handler reads by name, and a misspelled name is found when the route runs.
`Web.path<P>(method, pattern)` takes the parameters from a record `P` instead. The names in the pattern are
exactly the field names of `P`. The program stops at start-up, not at the first request, when they differ:

```gizlang
import * as Http from "gizlang:http";
import * as Web from "gizweb";

type Site = { name: string; };
type Thread = { target: string; page: i64; };

/** The route of a page of a thread. */
export function threadRoute(): Http.Route<Web.Handle<Site>> {
    const thread = Web.path<Thread>("GET", "/t/{target}/{page}");
    print(Web.link(thread, { target: "abc", page: 3 })!);   // /t/abc/3
    return Web.typed(thread, (site: Site, request: Http.Request, p: Thread): Http.Response => {
        return Http.text(Http.STATUS_OK, p.target + " " + string(p.page));
    });
}
```

- A field type is checked and converted like a form field: `page` is a number, and `/t/abc/x` answers
  `400` with the field that is wrong.
- `Web.link(path, params)` makes the URL and escapes each segment. A misspelled field is a compile error
  (`E_FIELD`), and a link cannot name a parameter the route does not have.
- Use the same `path` value for the route and for its links.

### Make layouts and components

A component is a function. It takes a props record, and, if it has children, an `Html` value.
It returns `Html`.

```gizlang
import {Html} from "gizlang:html";

type LayoutProps = { title: string; };
export function Layout(p: LayoutProps, children: Html): Html {
    return <html lang="en">
        <head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>{p.title}</title></head>
        <body><header><a href="/">Notes</a></header><main>{children}</main></body>
    </html>;
}
```

- Call it as `<Layout title="Notes">…</Layout>`. A props value in braces is a typed expression:
  `<Counter start={3}/>`.
- A nested layout is a component that calls another component.
- Lists: `{each(items, (n: Note) => <li>{n.title}</li>)}`.
- Conditions: `{when(condition, () => <p>…</p>)}`.
- A component has no state and no lifecycle. It runs on the server for each request.
- Text that you trust as HTML goes in with `raw(text)` from `gizlang:html`. Do not use it for
  user input.

### Use Web Components

Use a Web Component when a part of the page needs browser behavior. The `Shadow` component of
`gizlang:html` writes a declarative shadow root. The browser makes the shadow DOM from the HTML,
before scripts run. Keep the meaningful content in light DOM, and show it through a `<slot>`.

`howto/components.giz`:

```gizlang
import {System} from "gizlang:system";
import {Result} from "gizlang:core";
import * as Http from "gizlang:http";
import {Html, Shadow} from "gizlang:html";

type CounterProps = { start: i64; };
/** A Web Component rendered on the server: the shadow root is in the HTML. */
function Counter(p: CounterProps, children: Html): Html {
    return <note-counter>
        <Shadow delegatesFocus={false}><button type="button">+1</button> <output>{p.start}</output> <slot/></Shadow>
        {children}
    </note-counter>;
}
type Endpoint = | { kind: "home"; } | { kind: "asset"; };
function main(sys: System): i64 {
    const routes = Http.router([Http.Route("GET", "/", Endpoint.home()), Http.Route("GET", "/static/*path", Endpoint.asset())])!;
    const listener = sys.listen("127.0.0.1", 61104)!;
    Http.serve(listener, (request: Http.Request): Http.Response => {
        match (routes.resolve(request)) {
            Result.err(response) => { return response; }
            Result.ok(found) => {
                match (found.target) {
                    Endpoint.home() => {
                        const view = <html lang="en"><head><meta charset="utf-8"/><title>Counter</title>
                            <script type="module" src="/static/counter.js"></script></head>
                            <body><Counter start={3}><span>clicks</span></Counter></body></html>;
                        return Http.Response(200, [Http.Header("content-type", "text/html; charset=utf-8")], b"<!doctype html>" + view.markup);
                    }
                    Endpoint.asset() => {
                        const dir = sys.directory("howto/public")!;
                        const response = Http.file(dir, request, found.params.get("path"), sys.time());
                        dir.close();
                        return response;
                    }
                }
            }
        }
    })!;
    return 0;
}
```

`howto/public/counter.js` is a plain JavaScript module in a static folder:

```js
// Upgrades the server-rendered <note-counter>. The shadow root already exists.
customElements.define("note-counter", class extends HTMLElement {
  connectedCallback() {
    const output = this.shadowRoot.querySelector("output");
    this.shadowRoot.querySelector("button").addEventListener("click", () => { output.value = String(Number(output.value) + 1); });
  }
});
```

Run `giz howto/components.giz`, and open <http://127.0.0.1:61104/>. The server sends:

```text
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Counter</title><script type="module" src="/static/counter.js"></script></head><body><note-counter><template shadowrootmode="open" shadowrootclonable><button type="button">+1</button> <output>3</output> <slot></slot></template><span>clicks</span></note-counter></body></html>
```

In a Chromium browser, the element had its shadow root from the HTML. After two clicks, the output showed
`5`, and the light DOM `<span>` still said `clicks`.

- The script does not make the DOM again. It only adds behavior to the DOM that the server sent.
- With GizWeb, browser code can also be Gizlang: `client` compiles it to WebAssembly, and a live
  update replaces it in place.

### Handle forms and validate input

GizWeb derives the whole pattern from one record type ([Step 3](#step-3-write-the-pages-and-serve)):
the form, its parser and its validation. Without a model, the pattern is:

1. A native `<form method="post" action={…}>`, with its URL from a route declaration.
2. A `POST` route. Its handler reads the fields from `request.body`.
3. Validation on the server. On failure, `422` and the same form, with the typed values and an
   error message. On success, `303` to the result page.

`examples/web/http` is a complete app of this kind.

- `Http.decode(text, true)` decodes one form value (`+` is a space). It gives none for a bad
  escape.
- `gizlang:http` limits a request body to `Http.MOST_BODY` bytes (16 MiB).
- `Forms.form(shape, fields)` writes the form of a Shape, and `Web.inputs(request, shape)` reads and
  checks it. A file field (`@File`) makes the form multipart ([Take files in a form](#take-files-in-a-form)).

For a form that changes data, also check that the request comes from your site. A browser sends
`Sec-Fetch-Site` and `Origin` headers. Read them with `Http.header(request.headers, name)`.
Cookies from `Http.setCookie` are `SameSite=Lax`, so other sites do not send them with a cross-site
`POST`.

### Take files in a form

Mark a text field with `@File(accept, maxBytes, multiple)`. Its form control is then a file input, and the form is
`multipart/form-data`. The field holds the file name (a JSON list of names with `multiple`), so a Shape checks that a
file was chosen like any other value.

```gizlang
import {Option} from "gizlang:core";
import {Submit, File} from "gizdb:model";

@Submit("Send")
export type Attach = {
    note: string;
    @File("image/*,.pdf", 5000000, false)
    picture: string;
    @File("", 0, true)
    more: Option<string>;
};
```

- `Web.inputs(request, shape)` reads the form and checks each file against `accept` and `maxBytes`. A file in a field
  that takes none, or two files in a single-file field, is a `422` for that field.
- `Web.uploads(request, shape)` gives the files: `Upload { field, name, mediaType, size, bytes }`.
- The kernel sends a form with a file input as `FormData`. A file dropped or pasted on the form fills its input. A form
  posted without script works the same.
- The body is read whole (`Http.MOST_BODY`). A larger upload is streamed through `Serving.uploads`; store the bytes
  with Storage.

### Keep styles and scripts in files

Write CSS and JavaScript as files and import them as text. `Assets.minifyStyle` and `Assets.minifyScript` make the
served form.

`site.css`:

```css title="site.css"
body { margin: 0; font-family: system-ui, sans-serif; }
```

`style.giz`:

```gizlang
import {text as siteStyle} from "./site.css";
import {Assets} from "gizweb";

export function style(): string { return Assets.minifyStyle(siteStyle); }
```

For an app with many files, `Assets.build(sources, roots, minify, maps)` reads the whole graph:

- a script imports another by its logical name (`import {a} from "app/a.js"`); `import()` makes a lazy module;
- a style sheet may `@import` another (inlined) or `layer()` it; `*.module.css` renames its classes and
  `:global(x)` keeps one;
- `var(--x)` with no fallback and no declaration, an import cycle, an import of a file that is no source, and a source
  that no root reaches are problems, not silent output;
- `graph.audit(forbidden)` reads every served file for texts that must not leave the server (a secret, `/home/`).

`Web.sources(shell, graph, entries)` links the sheets, the import map, `modulepreload` and the entry scripts into every
page.

### See an edit at once

Under `giz live` on this machine, a page asks `/_giz/dev` which generation serves. A new generation updates the
page in place, as a link does (the stylesheets sync, the form values stay); a changed kernel reloads it. If the new
generation cannot start, or an edit does not compile, its reason or the compiler's error covers the page until an
edit fixes it, while the generation before keeps serving. The terminal that runs `live` shows the same error.

`Web.doctor(shell, graph, forbidden)` lists what is wrong with the setup (an empty language or name, a graph problem, a
served file over `Web.HEAVY_BYTES`), `Web.doctorReport(found)` prints it, and `Web.weights(graph)` sizes each file.
Run them in a test so a regression fails the suite.

### Answer slow or broken calls

A handler call that runs longer than `Serving.limitMs` is cancelled and answers `504`; a fault answers `500`. A
browser gets a page and a program gets the JSON error that every refusal uses.

### Update a page before the server answers

Put attributes on a form (`data-a`):

- `data-optimistic="KEY=FIELD KEY@ATTRIBUTE=FIELD"` shows the form's `FIELD` in the element keyed `KEY` (as its text, or
  as `ATTRIBUTE`) at once. If the action fails, the old value comes back; the server's patches replace it when it
  succeeds.
- `data-retry="N"` sends the action again after a network failure (not after a rejection), 300 ms, 600 ms, ... apart.
- A `data-cancel` control in a busy form stops its request.
- `data-state` on the form is `busy`, `retrying`, `ok`, `error` or `cancelled`, for CSS.
- A rejected field gets `aria-invalid`, its message is in an `aria-live` slot that the control names with
  `aria-describedby`, and the first bad control takes the focus.

### Use the form and interaction components

`Web.Field`, `DateField`, `Combobox`, `Dialog`, `Menu` and `Chart` use the native elements (`input`, `datalist`,
`dialog`, `popover`, `svg` with a table for assistive technology), so they work without script, and the browser
supplies the keyboard, the language and right-to-left layout. `Combobox` adds the `giz-combobox` element
(`Web.comboboxScript()`, register it with `Web.enhancement`): the arrow keys move, Enter chooses, Escape closes, and an
IME composition is left alone. `Web.uiStyle()` covers dark mode, `prefers-contrast`, `forced-colors`,
`prefers-reduced-motion`, right-to-left and print.

### Give an agent the contract

`Web.contractRoute(shell, handlers, routes)` serves `/_contract`: the version, the routes, the actions (the same
list as the agent tools at `/_tools`), the subscription path and the served files. It is made from the declarations,
so it cannot disagree with them.

### Serve HTML without hydration

Each page in this guide is complete HTML from the server. The pages in the tutorial have no
`<script>` element. There is no client component tree, no serialized props and no hydration
step.

- A link is a normal `<a href>`. A form is a normal `<form>`. The tutorial pages contain no
  script, thus they work the same with JavaScript disabled.
- Add browser behavior only where it is necessary, as a Web Component
  ([Use Web Components](#use-web-components)).
- Static content is static bytes. A component does not go to the browser.

### Stream a response

A handler can return `ReplyPlan.progressive(...)` in place of a `Response`. The server sends the
head and each piece when you write it. Use it to send the start of a page before slow work ends.

`howto/server.giz` (this file is also used for static files and sessions below):

```gizlang
import {System} from "gizlang:system";
import {Result, Option} from "gizlang:core";
import * as Http from "gizlang:http";
import * as Task from "gizlang:task";
import * as Security from "gizlang:security";

type Endpoint = | { kind: "asset"; } | { kind: "login"; } | { kind: "me"; } | { kind: "clock"; };
type Site = { sys: System; routes: Http.Router<Endpoint>; secret: bytes; };

function handle(site: Site, request: Http.Request): Http.ReplyPlan {
    match (site.routes.resolve(request)) {
        Result.err(response) => { return response.plan(); }
        Result.ok(found) => {
            match (found.target) {
                // Static files: ETag, Last-Modified, ranges and HEAD come from Http.file.
                Endpoint.asset() => {
                    const dir = site.sys.directory("howto/public")!;
                    const response = Http.file(dir, request, found.params.get("path"), site.sys.time());
                    dir.close();
                    return Http.withHeader(response, "cache-control", "public, max-age=3600").plan();
                }
                // Sessions: a signed token in an HttpOnly cookie.
                Endpoint.login() => {
                    const token = Security.token(site.secret, "kim", site.sys.time());
                    return Http.withHeader(Http.redirect(Http.STATUS_SEE_OTHER, "/me"), "set-cookie", Http.setCookie("session", token, Option.none<i64>(), false)!).plan();
                }
                Endpoint.me() => {
                    match (Security.verified(site.secret, Http.cookie(request, "session") ?? "", site.sys.time())) {
                        Option.some(signed) => { return Http.text(200, "Signed in as " + signed.who).plan(); }
                        Option.none() => { return Http.text(401, "Sign in first").plan(); }
                    }
                }
                // Streaming: the head and each piece go to the client when they are written.
                Endpoint.clock() => {
                    return Http.ReplyPlan.progressive((exchange: mut Http.Exchange): Result<bool, Http.HttpError> => {
                        try exchange.respond(200, [Http.Header("content-type", "text/html; charset=utf-8")], Http.UNKNOWN_LENGTH);
                        try exchange.write(<p>Loading…</p>.markup);
                        for (const n of range(1, 4)) {
                            Task.sleep(500);
                            try exchange.write(<p>Tick {n}</p>.markup);
                        }
                        return exchange.finish();
                    });
                }
            }
        }
    }
}
function main(sys: System): i64 {
    const routes = Http.router([Http.Route("GET", "/static/*path", Endpoint.asset()), Http.Route("POST", "/login", Endpoint.login()),
        Http.Route("GET", "/me", Endpoint.me()), Http.Route("GET", "/clock", Endpoint.clock())])!;
    const site: Site = { sys: sys, routes: routes, secret: sys.randomBytes(32)! };
    const listener = sys.listen("127.0.0.1", 61103)!;
    Http.serve(listener, (request: Http.Request): Http.ReplyPlan => { return handle(site, request); })!;
    return 0;
}
```

Run `giz howto/server.giz`. `Http.serve` accepts a handler that returns `Response` or `ReplyPlan`. A
buffered response becomes a plan with `.plan()`.

```sh
curl -N --trace-time --trace-ascii - -o /dev/null http://127.0.0.1:61103/clock
```

```text
0000: HTTP/1.1 200 OK
0000: content-type: text/html; charset=utf-8
0000: date: Wed, 14 Oct 2026 02:01:14 GMT
0000: transfer-encoding: chunked
20:43:38.793436 <= Recv data, 17 bytes (0x11)
0000: <p>Loading...</p>
20:43:39.294065 <= Recv data, 13 bytes (0xd)
0000: <p>Tick 1</p>
20:43:39.794659 <= Recv data, 13 bytes (0xd)
0000: <p>Tick 2</p>
20:43:40.295156 <= Recv data, 13 bytes (0xd)
0000: <p>Tick 3</p>
```

The pieces arrive 500 ms apart. (`curl --trace-ascii` shows the non-ASCII `…` as `...`.)

- Make all decisions that change the status or the headers before `respond`. After `respond`,
  the status is sent.
- Call `exchange.finish()` to end the response.
- With GizWeb, `stream` places pieces into the page out of order, by key, through the 1.2 KB
  page kernel.

### Live updates

:::status{of="gizweb"}
:::

A GizWeb page follows the rows that it shows. When a row commits, the server sends keyed DOM
patches for the changed elements only, and the page keeps typed text and focus.

1. `Data.follow(sys, db, shape, filter, keys)` opens a watch on the rows, before the page reads them.
   It gives the id of the watch.
2. The element that shows the rows gets the attributes of `Web.live(out, topic, id)`.
3. The page kernel long-polls `/_s`. `Web.subscribed` answers with the patches of the changed rows.

With `gizlang:http` alone, use a streamed response ([Stream a response](#stream-a-response)) for a page
that changes while it is open.

### Serve static files

`Http.file(dir, request, path, nowMs)` serves a file below a directory, as nginx serves `root`.
In `howto/server.giz` above, the route `GET /static/*path` serves the folder `howto/public`. For
this example, `howto/public/site.css` contains `body { font-family: system-ui; }`.

```sh
curl -i http://127.0.0.1:61103/static/site.css
curl -i -H 'If-None-Match: "6accc7c7-21"' http://127.0.0.1:61103/static/site.css
curl -i http://127.0.0.1:61103/static/%2e%2e/routes.giz
```

```text
HTTP/1.1 200 OK
content-type: text/css; charset=utf-8
last-modified: Mon, 12 Oct 2026 11:43:03 GMT
etag: "6accc7c7-21"
accept-ranges: bytes
cache-control: public, max-age=3600
date: Wed, 14 Oct 2026 02:01:14 GMT
content-length: 33

body { font-family: system-ui; }

HTTP/1.1 304 Not Modified
content-type: text/css; charset=utf-8
last-modified: Mon, 12 Oct 2026 11:43:03 GMT
etag: "6accc7c7-21"
accept-ranges: bytes
cache-control: public, max-age=3600
date: Wed, 14 Oct 2026 02:01:14 GMT

HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
date: Wed, 14 Oct 2026 02:01:14 GMT
content-length: 22

Forbidden path segment
```

- `Http.file` sets the media type, `ETag` and `Last-Modified`. It answers conditional requests,
  `HEAD` and one byte range.
- A path cannot leave the directory or follow a link.
- `Http.withHeader` adds a header, for example `cache-control`.
- `Http.asset` gives a file a content-hashed name, so it can be cached for a year.

### Build static pages

`Web.prerender(answer, paths, dir)` asks `answer` for each path with a `GET`, as a browser does,
and writes the answers to `dir`. The same routes and the same HTML writer make the pages. Run it when you deploy:

```gizlang
import {System} from "gizlang:system";
import * as Http from "gizlang:http";
import * as Web from "gizweb";

/** The function that your server passes to `Http.serve`. */
function handle(request: Http.Request): Http.Response { return Http.text(Http.STATUS_OK, "a page"); }

function main(sys: System): void {
    sys.files()!.makeDirectories("public")!;
    const written = Web.prerender(handle, ["/", "/about", "/blog/first", "/feed.xml"], sys.directory("public")!)!;
    print(`${written.length} files`);
}
```

- `/about` becomes `about/index.html` and `/` becomes `index.html`. A path with a file extension keeps its name.
- A path that does not answer `200` stops the build with the path and the status in the error. A page that is
  missing never goes to production by chance.
- The result is the list of the names written.
- Serve the folder with `Http.fileReply` ([Serve static files](#serve-static-files)). No route handler runs and
  no context is needed. The pages have the bytes of the live pages.
- Only static pages belong here. A page that depends on the person who asks (the session, the query) is
  served by its route.
- GizStack does this for the content pages of its own site: it writes `Content.staticPaths` after each deploy and
  answers those paths from the files with `public, no-cache`.

### Sessions and sign-in

`gizlang:http` reads and writes cookies. `gizlang:security` signs a session token with HMAC-SHA256.
In `howto/server.giz` above:

- `POST /login` makes a token for `kim` with `Security.token(secret, principal, issuedMs)`. It
  sets it with `Http.setCookie`. The cookie has `Path=/`, `HttpOnly` and
  `SameSite=Lax`, and `Secure` when you pass `secure`; `giz doc Http.setCookie` gives the rules.
- `GET /me` reads the cookie with `Http.cookie`, and checks it with `Security.verified`. A token
  is valid for 30 days (`Security.SESSION_LIFE`).

```sh
curl -i http://127.0.0.1:61103/me
curl -i -c jar.txt -X POST http://127.0.0.1:61103/login
curl -b jar.txt http://127.0.0.1:61103/me
curl -b 'session=kim.1.00' http://127.0.0.1:61103/me
```

```text
HTTP/1.1 401 Unauthorized
content-type: text/plain; charset=utf-8
date: Wed, 14 Oct 2026 02:01:14 GMT
content-length: 13

Sign in first

HTTP/1.1 303 See Other
location: /me
content-type: text/plain; charset=utf-8
set-cookie: session=kim.1791805408905.8eb182c76ef99f65fc43c57dc879fc23f6c509eca36cead8ed57ce9b2d33a1f1; Path=/; HttpOnly; SameSite=Lax
date: Wed, 14 Oct 2026 02:01:14 GMT
content-length: 3

/me

Signed in as kim

Sign in first
```

- The example makes a new random secret at each start. Thus a restart signs out all users. In a
  real app, keep the secret in your database or in a protected file.
- The example signs in without a password, to show the cookie flow only. Use
  `Crypto.pbkdf2Sha256` from `gizlang:crypto` if you store passwords, or deploy to GizStack and
  use its sign-in.
- Add the `Secure` attribute to the cookie when you serve HTTPS.

### Protect routes with one policy

:::status{of="gizweb"}
:::

A rule that holds for many routes is written once. `Web.before(routes, check)` runs `check` ahead of
the handler of each route in the table. `check` gets the context, the request and the parameters.
It answers in the handler's place, or gives `Option.none()` to let the route run:

```gizlang
import {System} from "gizlang:system";
import {Option} from "gizlang:core";
import * as Http from "gizlang:http";
import * as Web from "gizweb";

type Site = { sys: System; secret: bytes; };

function page(path: string, text: string): Http.Route<Web.Handle<Site>> {
    return Http.Route("GET", path, Web.Handle((site: Site, request: Http.Request, p: Http.Params): Http.Response => {
        return Http.text(Http.STATUS_OK, text);
    }));
}
function signedIn(site: Site, request: Http.Request, p: Http.Params): Option<Http.Response> {
    match (Web.principal(site.secret, request, site.sys.time())) {
        Option.some(name) => { return Option.none(); }
        Option.none() => { return Option.some(Http.redirect(Http.STATUS_SEE_OTHER, "/login")); }
    }
}
export function routes(): Http.Router<Web.Handle<Site>> {
    let all = [page("/", "home"), page("/login", "sign in")];
    for (const r of Web.before([page("/account", "account"), page("/settings", "settings")], signedIn)) { all.push(r); }
    return Http.router(all)!;
}
```

A handler that returns a `Result` and meets a 401 sends a browser to `/login` through `Web.respond`.
A feature flag and a maintenance page are written the same way. For a rate limit see [Limit requests from one client](#limit-requests-from-one-client).

### Limit requests from one client

`Web.limited(routes, limit, windowMs, proxies)` answers `429 Too Many Requests` with `Retry-After` to a client
that makes more than `limit` requests within `windowMs` milliseconds. Other clients are not affected:

```gizlang
import * as Http from "gizlang:http";
import * as Web from "gizweb";

type Site = { name: string; };

function page(path: string, text: string): Http.Route<Web.Handle<Site>> {
    return Http.Route("GET", path, Web.Handle((site: Site, request: Http.Request, p: Http.Params): Http.Response => {
        return Http.text(Http.STATUS_OK, text);
    }));
}
export function routes(): Vec<Http.Route<Web.Handle<Site>>> {
    return Web.limited([page("/", "home"), page("/search", "results")], 600, 60000, ["10.174.0.1"]);
}
```

To limit a whole server, make a limiter once with `Web.limiter(limit, windowMs, proxies)` and call
`limiter.admits(request)` at the start of the handler. It gives `Option.some(response)` for a client over its limit.
The counts live in memory. To keep them over a restart, save `limiter.counted(wallMs, atLeast)` (the clients with at least `atLeast` requests in the window) and give them back with `limiter.restore(counts, wallMs)` at start. giz saves them every 15 s for the clients with at least 60 requests, in the `request_counts` table.

- The client is the address of the socket. The server puts it in the request header
  `x-peer-address` (`Http.PEER_HEADER`) and removes a header with that name that the client sent.
- Behind a reverse proxy, every request comes from the proxy. List its address in `proxies`. For a connection
  from a listed proxy, the client is the last value of `X-Forwarded-For`: the proxy added it. The values before it
  are what the client wrote, and they can be forged. A client that is not a listed proxy cannot choose
  its address with this header.
- A refused request is not counted. A client that is quiet for one window is forgotten.
- The counts are in the memory of the process. A restart resets them.
- GizStack reads the proxy addresses of its own site from `STACK_PROXIES` (see `stack/README.md`).

### Test a web app

A handler is a function from `Request` to `Response`. Call it in a test. No server, port or
browser is necessary. There are two kinds of test:

- A test file `X.test.giz` has functions that start with `test`. It checks what needs no
  `System`: a Shape (the checks of a form), a route table, a component. The compiler certifies a
  pure test, and a test that passed is not run again.
- A program with `main(sys: System)` checks what needs a database or a clock. It makes a GizDB
  folder in a temporary directory, builds the handler of one thread (`app(sys, engine)` in the
  tutorial) and calls it with requests. Step 4 of the tutorial shows both.

The router can be tested alone:

```gizlang
import {Result} from "gizlang:core";
import * as Test from "gizlang:test";
import * as Http from "gizlang:http";

function testEqualRoutesAreRefused(): void {
    match (Http.router([Http.Route("GET", "/users/{id}", 1), Http.Route("GET", "/users/{name}", 2)])) {
        Result.ok(router) => { Test.check(false); }
        Result.err(message) => { Test.includes(message, "Ambiguous route"); }
    }
}
function testHrefEscapesEachSegment(): void {
    const route = Http.Route("GET", "/users/{id}", 1);
    Test.equal(route.href(Http.Params(["id"], ["a/b c"]))!, "/users/a%2Fb%20c");
    match (route.href(Http.Params([], []))) {
        Result.ok(url) => { Test.check(false); }
        Result.err(message) => { Test.includes(message, "id"); }
    }
}
function testAnInvalidPatternIsRefused(): void {
    match (Http.router([Http.Route("GET", "/users/{id", 1)])) {
        Result.ok(router) => { Test.check(false); }
        Result.err(message) => { Test.includes(message, "Captures must occupy a complete segment"); }
    }
}
```

The first run gives `0 cached, 3 run, 0 failed`. A second run gives:

```text
3 cached, 0 run, 0 failed
```

A test that passed before, and that the compiler certifies as pure, is not run again (`cached`).
A failed assertion shows the test, the values and the line.

For browser journeys, use a browser automation tool against the running server.

### Deploy a web app

Build a native executable, and run it on a server:

```sh
giz build notes/main.giz -o notes-server
./notes-server
```

```text
listening on http://127.0.0.1:61100
```

- The executable runs on any x86-64 Linux server with glibc 2.35 or later: no build is for the CPU
  of the build computer.
- The data folder (`data` in the tutorial) is relative to the working directory. Run the executable
  from the folder that holds the data, and back up that folder.
- To deploy to GizStack instead, refer to [Deploy to GizStack](https://gizstack.com/docs/platform/deploy).

#### Serve HTTPS

`gizlang:tls` serves the same handler over TLS with your certificate. `Tls.credentials` reads the
PEM chain and the PEM private key from their paths.

For a local test, make a self-signed certificate in `tls/`:

```sh
mkdir -p tls
openssl req -x509 -newkey rsa:2048 -nodes -keyout tls/key.pem -out tls/cert.pem -days 1 -subj "/CN=localhost"
```

`howto/https.giz`:

```gizlang
import {System} from "gizlang:system";
import {Result} from "gizlang:core";
import * as Http from "gizlang:http";
import * as Tls from "gizlang:tls";

// giz howto/https.giz /absolute/cert.pem /absolute/key.pem
function main(sys: System): i64 {
    const args = sys.arguments();
    const credentials = Tls.credentials(sys, args[0], args[1])!;   // PEM chain (own certificate first), PEM key
    const listener = sys.listen("127.0.0.1", 61105)!;
    Tls.serve(listener, credentials, (request: Http.Request): Http.Response => {
        match (Http.query(request)) {
            Result.ok(q) => { return Http.text(200, "Hello over TLS. q=" + q.get("q")); }
            Result.err(e) => { return Http.text(e.status, e.message); }   // 400: a malformed query
        }
    })!;
    return 0;
}
```

```sh
giz howto/https.giz "$PWD/tls/cert.pem" "$PWD/tls/key.pem"
curl -k -i 'https://127.0.0.1:61105/?q=two+words'
```

```text
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
date: Wed, 14 Oct 2026 02:01:14 GMT
content-length: 27

Hello over TLS. q=two words
```

- The test used a self-signed certificate, thus `curl -k`. Use a certificate from a certificate
  authority on a public server.
- The server speaks HTTP/1.1, and HTTP/2 when the browser asks for it in the TLS handshake (ALPN). With
  `TLS_DOMAINS` set, `Web.listen` also serves HTTP/3 (QUIC, UDP on the same port number) to a browser that has seen the
  `alt-svc` header, when the server has OpenSSL 3.5 or later (`giz openssl` installs one). A handler
  does not change. Several requests of a browser share one connection and do not wait for each other.
  `gizlang:acme` gets certificates from an ACME authority such as Let's Encrypt.
### Share requests among servers

`Http.proxy(pool, upstream, request)` forwards a request to one server. `Http.balancer(pool, upstreams, holdMs)`
shares requests among several. `balancer.send(request)` answers like `Http.proxy`:

```gizlang
import {System} from "gizlang:system";
import * as Http from "gizlang:http";

function main(sys: System): void {
    const pool = Http.pool(sys, Http.poolDefaults());
    const servers = [Http.Origin(false, "10.0.0.2", 8080), Http.Origin(false, "10.0.0.3", 8080)];
    const balancer = Http.balancer(pool, servers, 5000);
    const listener = sys.listen("127.0.0.1", 61106)!;
    Http.serve(listener, (request: Http.Request): Http.Response => { return balancer.send(request); })!;
}
```

- The servers get requests in turn.
- A server that cannot be reached is not asked again for `holdMs` milliseconds. A request that is safe to
  repeat (`GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE`) goes to the next server.
- A `POST` is never sent twice. The client gets `502`. `502` is also the answer when no server can be reached.
- An answer of the server with a status such as `500` is passed to the client as it is. It does not
  count as a server that cannot be reached.


### Publish content

:::status{of="gizweb"}
:::

GizWeb has a CMS: Markdown collections checked by a model, menus from one `nav.md`, blocks such as
`:::cards` and `:::steps`, values bound from code, drafts and review, and `/llms.txt`,
`/llms-full.txt` and `sitemap.xml` for free. This site runs on it.

```gizlang
import {Dir} from "gizlang:system";
import {Html} from "gizlang:html";
import * as Model from "gizdb:model";
import * as Web from "gizweb";
import {Content} from "gizweb";

/** The pages under docs/ of ROOT, with the nav in nav.md: SHAPE types their frontmatter. */
export function docs(root: Dir, shape: Model.Shape, layout: (view: Content.View) => Html, labels: Web.Labels): Content.Loaded {
    const site = Content.Site("Docs", "https://example.com", "nav.md", "/play",
        [Content.Collection("docs", "/docs", shape, Content.Source.files("docs"))], [], [], layout, labels);
    return Content.load(root, site);
}
```

### Speak more than one language

A site of `.ghtml` pages is written in English. Each run of text a person reads is one message, together with its holes
and inline elements (`Hello <b>{name}</b>!`). A catalog `locales/TAG.json` beside the pages translates the messages:

```sh
giz i18n . --add ko      # create locales/ko.json with every message, untranslated
giz i18n .               # add new messages, remove the ones no page shows any more
giz i18n . --check       # in CI: fail when a catalog is incomplete or a translation does not fit
```

- A catalog maps each message, written in Unicode MessageFormat 2 (`Hello {#b}{$name}{/b}!`), to its translation. An
  untranslated message shows in English. The build fails on a translation whose holes or elements do not fit.
- `<template plural={count}>` with a `<template case="one">` for each case (and `case="=0"` for an exact count) is one
  message. Each translation uses its own language's plural cases: Korean has one, Arabic six.
- Each language has its own address: `/ko/pricing` is Korean, and `/pricing` English. Links in a page keep the
  language. The language picker's links add `?hl=ko`, and a cookie keeps the choice for a year. A browser that opens an
  English address while its languages say otherwise is sent to its own language's address (307); a crawler is not. Each
  page carries its `hreflang` alternates.
- An application behind a sign-in keeps one address for every language with `byCookie(site)` (`gizweb:pages`):
  the person's choice, then the browser's `Accept-Language`, picks the language.

## Explanation: how GizWeb differs from Next.js

### Server HTML is the state

In Next.js, the server renders React components to HTML. Then the browser downloads the
component code and the serialized props, and hydrates the page: React builds its tree again
and attaches the event handlers.

In GizWeb, a component is a function that writes escaped HTML bytes on the server. Nothing
goes to the browser except the HTML, and the scripts that you add yourself. There is no hydration
step, because there is no client tree to build. A page works before any script loads, and it
works with JavaScript disabled. Interaction is added at small boundaries, with native controls
and Web Components that use the DOM that the server sent.

The cost is that a page does not change in the browser without a new request, a stream or your
own script. GizWeb keeps the authoritative state on the server, and the browser keeps only
local interaction state.

### Routes are data, and their targets are typed

A Next.js route is a file path. A Remix route is a module in a route config. A GizWeb route
is a value: method, pattern and a typed target. The router compiles all declarations one time,
and refuses ambiguity. Thus:

- With `Http.Router<Endpoint>`, the compiler checks that `match (found.target)` handles each endpoint. With
  `Web.route`, the target is the handler, and `Web.serve` calls it with the context of the app.
- Links come from the same declaration (`route.href(params)`), so a changed pattern changes each link.
- One policy runs ahead of many routes (`Web.before`).
- 404, 405 with `Allow`, and `HEAD` come from the router, not from each handler.

### Actions are ordinary typed functions

A Next.js Server Action is a function that the framework calls through a generated endpoint. In
GizWeb, the form posts to a declared route, and the handler is an ordinary function with
typed inputs and a typed `Response`. The form works without JavaScript. Validation stays on the
server, and the answer follows Post/Redirect/Get: `303` on success, `422` with the form on
failure.

GizWeb goes one step further: an Action is derived from one record type, with its validation, its
form, its JSON answer and its agent tool description ([GizWeb](https://gizstack.com/gizweb)).

### One process, native code

A GizWeb app compiles to one native executable. The HTTP server, the router, the HTML
writer and the database driver are in the same program. There is no Node.js runtime and no
separate bundler.

## Known problems

| Problem | Effect | What to do |
| --- | --- | --- |
| `giz live` publishes a new generation, but a running `Http.serve` server keeps the old answers. | Code changes do not show until a restart. | Restart the server, or serve with `Web.listenOn` and a `Web.generation` ([See an edit at once](#see-an-edit-at-once)). |
| When one test fails, the summary can count more failed tests than the tests that failed (`3 run, 2 failed` for one failure). | The count is wrong. The `fail` lines are correct. | Read the `fail` lines. |

## Where to go next

- [Command line guide](command-line.md): `check`, `test`, `build`, `live` and packages.
- [Start with Gizlang](../start.md): the Playground and the markup example.
- [Packages](packages.md): split an app into modules with a pinned manifest.
- [GizWeb](https://gizstack.com/gizweb): models, Actions, live views and the CMS.
- [Model your data](model.md): the DAL, typed queries, migrations and access.
- The official examples (`examples/`): `examples/language`, `examples/web/http` and `examples/web/notes`. `web/comments` is the complete reference app.
- [Deploy to GizStack](https://gizstack.com/docs/platform/deploy): your app at its own address.
