@@ -0,0 +1,527 @@
1+# Jibb — an Elm-architecture UI language for Urbit
2+
3+Oct 29, 2026 · @iko
4+
5+## Summary
6+
7+Jibb is a Hoon dialect for Urbit frontends. `.jibb` files live in the desk under the `%jibb` mark; each desk's vendored Jibb compiles them, run by one shared host, so `|commit` is the only build step. Elm architecture, a small layout vocabulary, and three stages fixed by module kind:
8+
9+- **Build**, per commit: pages without data, as HTML, plus the stylesheet.
10+- **Publish**, per request, in the host: pages with data, from a fresh scry. The host serves every page, to the owner only.
11+- **Client**, in the browser: islands' `update`, `subs` and `view`, and stores, as JavaScript.
12+
13+A page without islands ships no JavaScript.
14+
15+**Goals**: no prebuild; familiar to Hoon programmers; a typed Elm architecture; Hoon's data end to end; no HTML or CSS outside the theme; static by default.
16+
17+**Non-goals**: agent logic, refinement types, transactions, JavaScript interop beyond Jibb's effects.
18+
19+*Name*: four letters like the vanes, after the jib, a boat's front sail.
20+
21+## What Urbit allows
22+
23+Everything needed exists. The one limit: marks can't follow imports, so the host compiles.
24+
25+| Mechanism | Gives Jibb | Limit |
26+| --- | --- | --- |
27+| [Marks](https://docs.urbit.org/urbit-os/kernel/clay/marks) | `%jibb` stores text, merged by line | A conversion can't scry, so can't follow imports |
28+| Clay | The host reads each desk's `/ui`, static files included, and hears of commits | Every version of every file is kept |
29+| Gall | One host agent serves every Jibb desk | Agent names are ship-wide |
30+| [Eyre `%set-response`](https://github.com/urbit/urbit/releases/tag/urbit-os-v2.139) | Responses without an Arvo event | Keyed by the full request target; entries outlive their agent; auth-flagged entries give non-owners a bare 403 |
31+| [Eyre noun channels](https://github.com/urbit/urbit/pull/6472) | Pokes and watches as nouns | Needs `jam`, `cue`, `@uw` and an event stream over `fetch` in the browser |
32+| [Docket](https://docs.urbit.org/build-on-urbit/userspace/dist/docket) | `site+/path`: an agent-served site | One `site` or glob clause per desk |
33+
34+## Prior art
35+
36+| Project | What it is | Jibb takes |
37+| --- | --- | --- |
38+| [Sail](https://docs.urbit.org/hoon/sail) | Hoon's HTML markup | Rendering in the agent |
39+| [Udon](https://docs.urbit.org/hoon/udon) | Markdown-like source under its own `%udon` mark, compiled by `;>` | A source language under its own mark |
40+| [Rudder](https://github.com/Fang-/suite) | Routing for Sail pages | Page routing |
41+| [Mast](https://github.com/R-JG/mast) | Display state in the agent, HTML diffs over a channel | One generic client script; channels as transport |
42+| [Docket](https://docs.urbit.org/build-on-urbit/userspace/dist/docket) | One agent serving every desk's front end | One shared host |
43+| [Elm architecture](https://guide.elm-lang.org/architecture/) | `Model`, `Msg`, `update`, `view`, [effects](https://guide.elm-lang.org/effects/), [keyed nodes](https://guide.elm-lang.org/optimization/keyed.html) | The program model, effects as data, keyed diffing in `each` |
44+| [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) | Layout without HTML or CSS | Rows, columns, `el`, wrapping, paragraphs, padding, fill |
45+| [elm-pages frozen views](https://elm-pages.com/docs/frozen-views/) | `Html Never` content kept out of the bundle | `(element !!)` |
46+| [Astro islands](https://docs.astro.build/en/concepts/islands/) | HTML by default; marked components hydrate | Islands |
47+| [Qwik resumability](https://qwik.dev/docs/concepts/resumable/) | State serialised into HTML; the client resumes | A jammed model; `init` never runs in the browser |
48+| [@urbit/nockjs](https://github.com/urbit/nockjs), [@urbit/aura](https://github.com/urbit/aura-js) | Nouns and aura printing in JavaScript; aura-js lacks `@dr` | References for Jibb's own codec |
49+
50+## Architecture
51+
52+Clay stores files; each desk's compiler, run by the shared host, places each module at its kind's stage; only island and store code reaches the browser.
53+
54+[embedded content: Jibb pipeline · source to browser in three stages\]
55+
56+## The language
57+
58+Hoon's runes and literals in an indentation layout, over Hoon's nouns and molds, so pokes, facts and scries need no conversion. The **Grammar** tab defines it.
59+
60+### Importing Hoon types
61+
62+`/-` and `/%` translate each mold a module uses from `/sur` files and marks:
63+
64+| Hoon | Jibb |
65+| --- | --- |
66+| `$%`, `$?` | `$?` |
67+| `$@` | `$?` with an atom case |
68+| `$^` | `$?` if its cases differ as nouns, else an error where used |
69+| `$~` | Dropped |
70+| `map`, `set`, `qeu`, `jar`, `jug` | The same, by name, checked with `apt` on arrival |
71+| `mop` | `(mop K V)`, read with `on` |
72+| Other `$\|` | Its base type |
73+| `%0`, `%&`, `%\|` | Tags |
74+| Recursive molds, such as `json` | Recursive, matched by name |
75+| Cores, gates, vases, signed and float auras | An error where used |
76+
77+Unused molds are skipped, so unsupported constructs elsewhere in a `/sur` file are fine.
78+
79+## Prelude
80+
81+Hoon's standard library regrouped, written in Jibb and vendored as `/lib/jibb/prelude`; an island's bundle carries only what it uses. Only the prelude declares natives, Jibb's jets, and private helpers; Jibb-written gates run unjetted on the ship; *proposed*: list gates such as `turn` become natives that take gates (Core language). It also holds `eq`, `not`, `page`, `program`, `store`, `pure`, `island`, and the layout elements and molds.
82+
83+| Group | Hoon | Jibb |
84+| --- | --- | --- |
85+| Arithmetic | [1a](https://docs.urbit.org/hoon/stdlib/1a), [2g](https://docs.urbit.org/hoon/stdlib/2g) | Natives over aura variables: `add`, `sub`, `div`, `mod`, `gth`, `max`, `pow` |
86+| Units | [2a](https://docs.urbit.org/hoon/stdlib/2a) | `fall`, `need`, `bind`, `biff` |
87+| Lists | [2b](https://docs.urbit.org/hoon/stdlib/2b) | `turn`, `weld`, `snag`, `lent`, `sort`, `skim`; `(roll xs init f)` takes a start |
88+| Maps, sets, queues | [2h](https://docs.urbit.org/hoon/stdlib/2h)–[2m](https://docs.urbit.org/hoon/stdlib/2m), [2o](https://docs.urbit.org/hoon/stdlib/2o) | Plain gates over Hoon's trees, such as `(get.by m k)`; `rep.by` and `rep.in` take a start; ordered maps through lull's `on`, as `(get.on lte m k)` |
89+| Function helpers | [2n](https://docs.urbit.org/hoon/stdlib/2n) | `same`, `head`, `tail`, `cork`, `cury` |
90+| Text | [4b](https://docs.urbit.org/hoon/stdlib/4b) | `crip`, `trip`, `cass`, `cuss`, `trim`, `tufa`, `tuba` |
91+| Atom printing | [4a](https://docs.urbit.org/hoon/stdlib/4a), [4j](https://docs.urbit.org/hoon/stdlib/4j)–[4m](https://docs.urbit.org/hoon/stdlib/4m) | `scot`, `scow`, `slav`, `slaw`. `print-date` prints Urbit-style in the theme's zone, to the minute, as `~2026.10.29..14.30`, from a zone table jibb-dev keeps current. Numbers print as Hoon does, as `1.000` |
92+| Time | [3c](https://docs.urbit.org/hoon/stdlib/3c) | `yore`, `year` |
93+| Data molds | [1c](https://docs.urbit.org/hoon/stdlib/1c), [2q](https://docs.urbit.org/hoon/stdlib/2q), `coin` and `dime` from [3g](https://docs.urbit.org/hoon/stdlib/3g) | `unit`, `list`, `pair`, `tape`, `path` and the rest |
94+| Bits | [2c](https://docs.urbit.org/hoon/stdlib/2c), [2d](https://docs.urbit.org/hoon/stdlib/2d) | Natives; `met`, `cut`, `rap`, `rip` exposed. Jibb's `not` negates a loobean |
95+| Hashing and ordering | [2e](https://docs.urbit.org/hoon/stdlib/2e), [2f](https://docs.urbit.org/hoon/stdlib/2f) | Natives behind maps and sets; `aor` exposed |
96+| Signed and floats | [3a](https://docs.urbit.org/hoon/stdlib/3a), [3b](https://docs.urbit.org/hoon/stdlib/3b) | Left out; `@s…` and `@r…` auras are errors, imported ones included |
97+| Parsers | [4d](https://docs.urbit.org/hoon/stdlib/4d)–[4i](https://docs.urbit.org/hoon/stdlib/4i) | Left out |
98+| The rest | [1b](https://docs.urbit.org/hoon/stdlib/1b), [2p](https://docs.urbit.org/hoon/stdlib/2p), [3d](https://docs.urbit.org/hoon/stdlib/3d), [3f](https://docs.urbit.org/hoon/stdlib/3f), [4c](https://docs.urbit.org/hoon/stdlib/4c), [4n](https://docs.urbit.org/hoon/stdlib/4n), [4o](https://docs.urbit.org/hoon/stdlib/4o), [5a](https://docs.urbit.org/hoon/stdlib/5a)–[5f](https://docs.urbit.org/hoon/stdlib/5f) | Left out; `jam`, `cue` and `fein` run inside the runtime |
99+
100+Maps are Hoon's trees on both sides, so equal maps are equal nouns. Requests check incoming maps and sets with `apt`.
101+
102+## The Elm architecture on Urbit
103+
104+A module's kind is the type of its `main`:
105+
106+| Kind | `main` | Runs | Ships JavaScript |
107+| --- | --- | --- | --- |
108+| Page | `(element !!)` | At commit | Only its islands' |
109+| Page with data | `(page load view)` | In the host, per request | Only its islands' |
110+| Island | `(program init update view subs)` | `init` and the first view with its page; the rest in the browser | Yes |
111+| Store | `(store init update)` | In the browser, shared by a page's islands | With its islands |
112+
113+A module without `main` is a library. The host reads a few other names per file kind, listed in the Grammar tab. Pages embed islands as `island main.todo-list ts`, an `(element !!)`:
114+
115+- `main.todo-list` is a record of exactly `init`, `update`, `view` and `subs`, whose types fix the data, model and messages. `subs` is required, as in Elm's `Browser.element`; `|=(m ~)` means none.
116+- `ts` is the island's data, checked against `init`'s sample.
117+- `(element !!)` means no messages reach the page. It also keeps islands out of islands, since an island's view is an `(element M)`.
118+
119+`init` runs on the ship at the page's stage, and the first view renders into the page with the model jammed in. On start, the browser runs `view` on that model, binds the result to the server's nodes by key, and replaces the subtree if they differ. Island data and models can't hold gates; the checker names the field.
120+
121+**Loading.** A page with data renders in the host per request, from a fresh scry.
122+
123+- `load`, `page`'s first argument, maps route parameters to one `ask`: `++ load |=(~ (todos.todo same))` without parameters.
124+- The host scries the ask's path per request, so the agent must answer a peek there; nothing is watched or kept. A path case can name a separate scry path.
125+- Pages read full state only; folds matter only to islands.
126+- Route parameters are `load`'s sample, bound by name and parsed by aura: `/ui/blog/_slug.jibb` has `++ load |=([slug=@ta] (post.api slug same))`.
127+- `(pure x)` is an ask without an agent; an app's catch-all page passes its path with it.
128+- A rejected path is a 404.
129+
130+Loads and renders run in Nock.
131+
132+**Access.** Every page is private: the host serves it per request, only to the owner, and sends anyone else to `/~/login?redirect=…`. Public pages are left for a later version.
133+
134+**Errors.** A rejected path, an unparseable parameter or a crashing render gets Jibb's error page, 404 or 500; no error is cached. A control rendered outside an island is rejected: at commit for pages without data, per request otherwise. There's no step limit yet, so an endless render stalls the ship, as in Hoon.
135+
136+- A crashing island shows the standard crash notice: in the page's HTML for `init` or the first view. In the browser, a crash in `update`, `view` or `subs` drops the message and keeps the model, DOM and watches.
137+
138+**Effects.** Commands and subscriptions are library values:
139+
140+```jibb
141++$ cmd |$ [M] $?
142+ [%poke p=poke reply=$-((unit @t) M)]
143+ [%go url=@t]
144+ [%wait gap=@dr msg=M]
145+ [%now reply=$-(@da M)]
146+ [%who reply=$-(@p M)]
147++$ sub |$ [M] $?
148+ [%watch a=(ask M)]
149+ [%every gap=@dr reply=$-(@da M)]
150+ [%url reply=$-(path M)]
151+```
152+
153+- `poke` and `ask` are opaque, built only by derived gates and `pure`. A constructor such as `(todo-action.todo [%add draft.m])` checks its value and supplies the agent and mark, so one island can poke several agents. The agent's mark rechecks on the ship; a nack's `tang` reaches the reply as text. Store constructors poke the store in the browser.
154+- A request such as `(todos.todo |=%got)` comes from a declared path or a store's `state`; its reply always gets full state. A path with diffs also takes a fold, `(changes.todo |=%got apply)`, which the runtime applies first. Facts are checked; a bad one crashes the reply, which counts as a crashed `update`.
155+- Islands read agents only by watching. A watch's first fact must be full state, so islands can't watch paths that need a separate scry path. The runtime acks facts, rewatches after a kick or a dropped connection and reseeds from the first fact; a refused watch shows the crash notice.
156+- Watches match by agent and path, timers by gap, and the newest reply is always used. After each `update`, `subs` is diffed to start and stop watches and timers. `%now` and `%every` give the time, `%who` the owner's `@p` from `/~/name`; `%wait` is a one-shot delay.
157+
158+Inputs send a message per `input` event, and one per IME composition, so validation runs as you type; only commands reach the ship. After every event, controls reset to the latest view, so a toggle whose poke fails unchecks. A field's text is rewritten only when it differs, which keeps the caret. Fields are read-only until their island starts.
159+
160+**Island lifecycle.**
161+
162+- `init` runs on the ship with the page's data and returns only the model, jammed into the page; the browser resumes from it at start. A reload starts from the model the page is served with.
163+- A crash in `update` drops the message, keeps the model and shows the standard notice.
164+- Stores share state between a page's islands. A store module, written like an API file, has `state`, `+$ poke` and `++ main (store init update)`, where `update` applies a poke. Islands import it with `/=` and use its derived gates: a constructor per poke type, and `(state.cart |=%got)`, whose first fact is the current state and each later fact the next.
165+- A store runs in the browser from its `init` when its first island starts, and lasts as long as the page. Islands keep what they watch in their own models.
166+- An app is a page whose view is one island under a catch-all route; its `load` passes the path with `pure`. The island's `%url` subscription delivers the path at start and after every change, back and forward included. `go` pushes history, and `go` or `link` within the section changes the island without a reload. Focus then moves to the new view's first heading, which screen readers announce.
167+
168+## Layout and styling
169+
170+Elements are ordinary gates with their own signatures. Containers take attribute lists, and `el` adds attributes to anything. Spaces and colours are theme tokens. Text elements come from `text`, as `(text "hi")`. The attribute union is `attr`, so helpers can declare `^- (list attr)`.
171+
172+| Element | Arguments | Purpose |
173+| --- | --- | --- |
174+| `row`, `column`, `wrap` | `attrs kids` | Containers |
175+| `stack` | `attrs kids` | A column, then a row from the `wide` breakpoint up |
176+| `el` | `attrs kid` | Adds attributes; `el`'s values win |
177+| `para` | `kids` | Flowing text |
178+| `text` | A cord | Text |
179+| `heading` | `level words` | `<h1>` to `<h6>` |
180+| `each` | `(list [key=* el=(element M)])` | Keyed children; repeated keys match in order |
181+| `table` | Columns `(list [header=@t width=length align=align])`, rows `(list [key=* cells=(list (element M))])` | A table with keyed rows |
182+| `image` | `src description`, with `src` an application path to a static image or `[url=@t width=@ud height=@ud]` | Boxed to its aspect ratio before loading, never wider than its container; static images are sized from the file; `""` marks it decorative |
183+| `link` | `url label` | See Links and URLs |
184+| `button` | `label msg` | A control |
185+| `toggle` | `on label msg` | A control |
186+| `field` | `label value reply` | A control; `reply` turns its text into a message, as `\|=%typed` |
187+| `form` | `msg kids` | A control; Enter in a field sends the nearest form's `msg` |
188+| `island` | `program data` | Embeds an island |
189+
190+Controls work only in islands; the host rejects a page that renders one outside an island.
191+
192+| Attribute | Values |
193+| --- | --- |
194+| `pad`, `gap` | `%s0` to `%s3`; `gap` on containers only |
195+| `width` | `[%chars n]`, `[%fill n]`, `%shrink`; `[%fill n]` below 1 counts as 1 |
196+| `align` | `%start`, `%center`, `%end` |
197+| `role` | `%body`, `%caption`, `%label` |
198+| `tone` | `%normal`, `%muted`, `%accent`, `%danger`; danger is also marked `!` and announced |
199+
200+The same attribute twice in a literal list is an error; otherwise the last wins.
201+
202+- Rows lay out left to right, centred vertically; columns top to bottom. Children take their content's width unless they fill.
203+- `[%fill n]` shares leftover space in proportion to `n`, `%shrink` takes the content's width, and `[%chars n]` is `n` widths of `0`.
204+- Empty elements take no room and no gap.
205+- Nothing exceeds the page's width: long words break, and wide content scrolls in its own box.
206+
207+The theme, `++ theme` in `/ui/theme.jibb`, uses the prelude's `theme` mold, with a default for desks without one. It requires `s1` to `s3` (`s0` is zero), five tones, with `normal` the text and `background` the page colour, the page's language, and the time zone dates print in. `wide` is the breakpoint where `stack` becomes a row. Dark tones go under `dark`, used when the reader prefers dark. An optional `font` is an application path to a static font. Tokens become CSS custom properties and `wide` a media query; values must be CSS lengths or colours, and each tone needs 4.5:1 contrast with its background.
208+
209+```jibb
210+++ theme ^- theme
211+ [ space=[s1="4px" s2="8px" s3="16px"]
212+ tones=[normal="#222" background="#fff" accent="#17734f" danger="#b3261e" muted="#5f5f5f"]
213+ wide="60rem"
214+ lang="en"
215+ zone="UTC"
216+ font=`%/static/fonts/inter.woff2
217+ dark=[tones=[normal="#eee" background="#111" accent="#4c9" danger="#f28b82" muted="#aaa"]]
218+```
219+
220+Style attributes compile to atomic classes in one stylesheet built from the theme, so islands can't compute a missing class. `[%chars n]` and `[%fill n]` become inline styles.
221+
222+### Links and URLs
223+
224+Both backends escape all text and attributes. In-app links are application paths, `%/blog` or `%/blog/(slug.p)`, checked at compile time against routes and static files; parameters print by aura, and the base path is implicit. They carry no query or fragment, so apps keep their state in the path.
225+
226+`link`, `image` and `go` also take two kinds of unchecked cord: a root-relative path to another app on the ship, such as `"/apps/groups"`, and an `https`, `mailto` or `tel` URL. A cord starting `//` or `/\` is neither. Any other cord, such as `javascript:`, is drawn as text, and `go` ignores it. External links open in a new tab with `noopener`. Pages send a Content-Security-Policy allowing scripts only from the ship.
227+
228+## Stages
229+
230+A module's kind fixes where it runs; nothing infers stages.
231+
232+| Stage | Runs | Where | Output |
233+| --- | --- | --- | --- |
234+| Build | Pages without data | The host, per commit | HTML and the stylesheet |
235+| Publish | Pages with data | The host, in Nock, per request | HTML |
236+| Client | Islands' `update`, `subs` and `view`; stores | The browser, in JavaScript | DOM patches from a keyed diff |
237+
238+- **Islands follow their page**: `init` and the first view run wherever the page renders, and the model is jammed into the HTML.
239+- **Re-render, then diff**: each message that changes the model re-runs `view`, and a virtual-DOM diff patches the DOM. Children of `each` are keyed, as in Elm's `Html.Keyed`, so focus survives reordering.
240+- **Functions have no stage**: a gate compiles for whichever side calls it.
241+- **No JavaScript needed**: pages render without it, and islands show their server-rendered state, with controls `aria-disabled` and fields read-only until they start.
242+- **Holes later**: compiling views to static HTML plus small functions of the model is an optimisation that changes no types or errors, so it waits.
243+
244+## The mark
245+
246+`%jibb` stores source as text and nothing else. Conversions can't follow imports, so the host compiles, and `|commit` never loads the compiler.
247+
248+### `/mar/jibb.hoon`
249+
250+```hoon
251+|_ src=@t
252+++ grab
253+ |%
254+ ++ mime |=([p=mite q=octs] q.q) :: from Unix on |commit
255+ ++ noun @t
256+ ++ txt of-wain:format
257+ --
258+++ grow
259+ |%
260+ ++ mime [/text/x-jibb (as-octs:mimes:html src)]
261+ ++ txt (to-wain:format src)
262+ ++ noun src
263+ --
264+++ grad %txt :: line diffs and merges, as %hoon does
265+--
266+```
267+
268+- Text, like `%hoon`: `|commit` never fails on a half-written file, and Clay merges by line.
269+- Compiled modules are cached by their hash, their imports' hashes, Hoon files included, and the desk's compiler, so a commit recompiles only what changed. Import cycles are errors.
270+- Relinking prints each error with its file, line, column and an excerpt.
271+- Each app desk vendors `jibb-dev`, as with `base-dev`: compiler, prelude, browser library, and the `%jibb` and static-file marks. It also needs base-dev's `txt`, `txt-diff`, `js`, `bill` and `kelvin` marks, docket's `docket-0`, and a `sys.kelvin`. The `%jibb` desk holds only the shared host and its threads.
272+
273+### Linking
274+
275+On every commit, the host reads each Jibb desk's `/ui`, and that desk's compiler rebuilds what changed and relinks, checking modules in dependency order and resolving `/-` and `/%` through Clay. There's no other build path: marks can't follow imports, and `/~` builds only `.hoon` files.
276+
277+The host is one Gall agent on the `%jibb` desk, serving every Jibb desk, so app agents stay plain Hoon; it never reads their state. A desk opts in by vendoring Jibb and naming its base path in docket's `site+`. The host is thin: it binds paths, watches desks, caches and serves. Each desk's Jibb builds and renders everything else, HTML shell, CSP and error pages included, through a few entry points fixed across releases, so desks upgrade independently. The host clears a desk's cache entries when it's suspended or uninstalled, and accepts pokes only from the ship.
278+
279+### Agent APIs
280+
281+Each agent Jibb talks to has `/ui/api/<agent>.jibb`, declaring its pokes in `+$ poke` and its paths in `+$ paths`, over types from the agent's own marks through `/%`. Importing it gives derived gates: a constructor per poke type, which pokes with that type's mark, and a request per path, which checks what comes back. Clashing derived names are errors.
282+
283+```jibb
284+:: /ui/api/todo.jibb
285+/- todo
286+/% %todo-action
287+/% %todos
288+/% %todo-update
289+/% %todo-item
290+
291++$ item item.todo
292++$ poke todo-action
293++$ paths $?
294+ [%todos /todos todos]
295+ [%changes /changes (facts todos todo-update)]
296+ [%todo /todo/[id=@ud] todo-item]
297+```
298+
299+A path case is a request name, a path and a type. A plain mold is full state, which a watch's first fact must carry. `(facts full diff …)` adds diffs, told apart by mark, and its request takes a fold, here `$-([[%todo-update todo-update] todos] todos)`. Pages scry the path, so the agent must answer a peek there. A case can name another scry path, as in `[%changes /updates (facts todos todo-update) scry=/todos]`, used only by pages.
300+
301+`poke` and `paths` are declarations, not molds. An API module re-exports its `/%` types, not `/-` faces or `/=` modules.
302+
303+Any mark name works: `/% %todo-action-1` gives the type `todo-action-1`, poked with `%todo-action-1`. Helper molds such as `item` come from the `/sur` file through `/-`. The marks are the agent author's:
304+
305+```hoon
306+:: /mar/todo-action.hoon, written with the agent
307+/- todo
308+|_ a=action:todo
309+++ grab |% ++ noun action:todo --
310+++ grow |% ++ noun a --
311+++ grad %noun
312+--
313+```
314+
315+Jibb calls this type `todo-action`, after its mark. Jibb's own types never cross to the ship, so they need no marks.
316+
317+### Routing by file path
318+
319+Pages route by their path under `/ui`; islands, stores and libraries are only imported.
320+
321+| File | URL |
322+| --- | --- |
323+| `/ui/index.jibb` | `/apps/my-app` |
324+| `/ui/blog/index.jibb` | `/apps/my-app/blog` |
325+| `/ui/blog/_slug.jibb` | `/apps/my-app/blog/{slug}`; `_` marks a parameter |
326+| `/ui/settings/__rest.jibb` | `/apps/my-app/settings` and below; `__` marks a catch-all, passed to `load` as a path |
327+
328+- One URL per page, with no trailing slash; other forms and query strings get the same page.
329+- A literal beats a parameter, which beats a catch-all. Two parameters at one level, `blog.jibb` beside `blog/index.jibb`, and a catch-all beside `index.jibb` are errors. Folders can be parameters: `/ui/_user/posts.jibb`.
330+- Parameters bind to `load`'s sample by name, which must have exactly those fields; a catch-all's is a `path`.
331+- `/apps/my-app/~jibb/` and `/apps/my-app/static/` belong to the host.
332+
333+Typed segments share one notation across routes, application paths and agent paths. `@ta` and `@tas` are raw text, checked as knots, as `/blog/my-post`; other auras print and parse as Hoon does, so `@ud` is `1.000`. The host splits the raw URL, so dots stay inside segments; a segment that doesn't parse is a 404. `%/todo/(id)` and `[%todo /todo/[id=@ud] todo-item]` print the same way.
334+
335+### Desk layout
336+
337+```
338+:: an app desk
339+/mar/jibb.hoon /mar/png.hoon … marks from jibb-dev
340+/lib/jibb.hoon /lib/jibb/prelude compiler and prelude from jibb-dev
341+/lib/jibb/runtime/js browser library from jibb-dev
342+/mar/txt.hoon /mar/js.hoon … marks from base-dev
343+/mar/docket-0.hoon docket's mark from landscape
344+/sys.kelvin the desk's kelvin
345+/sur/todo.hoon /mar/todo-*.hoon the %todo agent's types and marks
346+/app/my-app.hoon your agent, unchanged
347+/ui/api/todo.jibb the %todo agent's API
348+/ui/index.jibb /ui/blog/_slug.jibb pages
349+/ui/todo-list.jibb an island
350+/ui/stores/cart.jibb a store
351+/ui/theme.jibb theme
352+/ui/static/img/logo.png static files
353+/ui/tests/todo.jibb tests: each test- arm is one test
354+desk.bill %my-app
355+desk.docket-0 site+/apps/my-app, with docket's other clauses
356+
357+:: the %jibb desk, installed once per ship
358+/app/jibb.hoon the shared host
359+/ted/clear.hoon /ted/test.hoon -jibb!clear, -jibb!test
360+```
361+
362+The `%todo` and `%blog` agents run on their own desks; this desk copies their `/sur` and `/mar` files.
363+
364+## Client runtime and Eyre
365+
366+Islands compile to small JavaScript modules sharing one hand-written library; pages without islands load neither.
367+
368+- **Two backends.** Each module lowers to the core language (Core language), which both backends compile: Nock on the ship for rendering and `init`, with natives calling jets, and JavaScript in the browser for `update`, `subs` and `view`. Differential tests under Node, from jibb-dev, keep them in agreement.
369+- **Shared library**, under 20 KB: its own noun codec (`jam`, `cue`, `@uw`, treaps, aura printing), the channel client, a keyed DOM differ, stores and URL subscriptions. It reads events over `fetch`, since `EventSource` can't send `x-channel-format`, deletes its channel on `pagehide`, and opens a fresh one when a reconnect gets a 404.
370+
371+### What a page with an island looks like
372+
373+```html
374+<!doctype html>
375+<html lang="en">
376+<head>
377+ <meta charset="utf-8">
378+ <meta name="viewport" content="width=device-width, initial-scale=1">
379+ <title>My ship</title>
380+ <link rel="stylesheet" href="/apps/my-app/~jibb/s/0v1a2b3.css">
381+</head>
382+<body>
383+<main>
384+ … server-rendered HTML …
385+ <div data-jibb-island="0v4c5d6">
386+ … initial HTML …
387+ <script type="application/x-jibb-state">… jammed model, @uw-encoded …</script>
388+ </div>
389+</main>
390+<script type="module" src="/apps/my-app/~jibb/lib.0v7e8f9.js" async></script>
391+</body>
392+</html>
393+```
394+
395+The title is the first `heading 1`, and the language comes from the theme. `data-jibb-island` names the module by hash, so a shared island loads once; each instance keeps its state block in its own element. Every island starts as soon as the library loads.
396+
397+### Eyre integration
398+
399+- The host binds each desk's base path, from its `site+` clause, with `%connect`.
400+- The host serves every page itself, per request and only to the owner, sending anyone else to the login page; no page enters Eyre's cache.
401+- CSS, the library, island modules and static files sit at content-hashed, immutable paths, cached with `%set-response` and served without an Arvo event. They're public, since only pages carry data, and each build's files stay a day after replacement so open tabs keep working.
402+- A commit re-renders every page without data, which the host then serves from its copy.
403+- The cache is ship-wide and outlives its agent. The host caches only under each desk's base path and clears a desk's entries on suspend or uninstall; run `-jibb!clear` before uninstalling `%jibb`.
404+- Cache hits skip Eyre's other handling, such as HTTPS redirects and CORS.
405+
406+### Static files
407+
408+Images and fonts live under `/ui/static/`, committed with the desk, so installers get them; jibb-dev's marks store them as they are.
409+
410+- Pages refer to them by application path, as `(image %/static/img/logo.png "The logo")`. A missing file is a compile error, and images are sized from the file.
411+- Each file is served from Eyre's cache at a URL with a short hash of its bytes, such as `/apps/my-app/static/0v1.7rq2a/img/logo.png`, marked immutable; a changed file gets a new URL.
412+- Clay keeps every version, and cache hits ignore `Range`, so audio, video and large image sets stay out for now.
413+
414+## Worked examples
415+
416+One page per stage.
417+
418+### 1. Static: `/ui/about.jibb`
419+
420+```jibb
421+++ main view
422+
423+++ view
424+ column ~[width+[%fill 1] pad+%s3 gap+%s2] ~[
425+ heading 1 "About this ship"
426+ para ~[(text "Hosted on Urbit. This page ships no JavaScript.")]
427+ el ~[tone+%accent] (link %/blog "Read the blog")
428+```
429+
430+An `(element !!)`: build stage, one HTML file plus the stylesheet, rendered on commit, titled "About this ship".
431+
432+### 2. Published: `/ui/blog/index.jibb`
433+
434+```jibb
435+/- blog
436+/= api /ui/api/blog
437+/= card /ui/components/card
438+
439+++ main (page load view)
440+++ load |=(~ (posts.api same))
441+
442+++ view |= ps=(list post.blog)
443+ column ~[gap+%s2] (turn ps card.card)
444+```
445+
446+A `page`: each request scries `%blog` and renders in Nock.
447+
448+### 3. Interactive: `/ui/todo-list.jibb` and `/ui/index.jibb`
449+
450+```jibb
451+/= todo /ui/api/todo
452+
453+++ main (program init update view subs)
454+
455++$ model [todos=(list item.todo) draft=@t error=(unit @t)]
456++$ msg $?
457+ [%typed text=@t]
458+ %submit
459+ [%toggled id=@ud]
460+ [%got todos=(list item.todo)]
461+ [%acked err=(unit @t)]
462+
463+++ init |= ts=(list item.todo)
464+ [ts "" ~]
465+
466+++ update |= [=msg m=model]
467+ ?- msg
468+ [%typed t]
469+ ?- (gth (lent (tuba (trip t))) 80)
470+ %.y [m(draft=t error=`"Too long") ~]
471+ %.n [m(draft=t error=~) ~]
472+ %submit [m(draft="") ~[[%poke (todo-action.todo [%add draft.m]) |=%acked]]]
473+ [%toggled id] [m ~[[%poke (todo-action.todo [%toggle id]) |=%acked]]]
474+ [%got ts] [m(todos=ts) ~]
475+ [%acked err] [m(error=err) ~]
476+
477+++ subs |= m=model
478+ ~[[%watch (todos.todo |=%got)]]
479+
480+++ view |= m=model
481+ column ~[gap+%s2] ~[
482+ heading 2 "Todos"
483+ each (turn todos.m row-of)
484+ row ~[gap+%s1] ~[
485+ el ~[width+[%fill 1]] (field "New todo" draft.m |=%typed)
486+ button "Add" %submit
487+ el ~[tone+%danger] (text (fall error.m ""))
488+
489+++ row-of |= t=item.todo
490+ [id.t (toggle done.t text.t [%toggled id.t])]
491+```
492+
493+```jibb
494+/= todo /ui/api/todo
495+/= todo-list /ui/todo-list
496+
497+++ main (page load view)
498+++ load |=(~ (todos.todo same))
499+
500+++ view |= ts=(list item.todo)
501+ column ~[pad+%s3] ~[
502+ heading 1 "My ship"
503+ island main.todo-list ts
504+```
505+
506+| Part | Stage | Becomes |
507+| --- | --- | --- |
508+| The page's view, "My ship" | Publish | HTML from a fresh scry per request, served only to the owner |
509+| The island's `init` and first view | Publish, with its page | HTML in the page, with the jammed model |
510+| `update`, `subs`, `view` | Client | A JavaScript module, loaded with the page; model changes re-run `view` and patch the DOM |
511+| `[%add …]`, `[%toggle …]` | Ship | One poke each: an Arvo event per command, not per keystroke |
512+
513+### Opting in
514+
515+A desk becomes a Jibb app by vendoring jibb-dev and naming its base path in docket, here `site+/apps/my-app`. The host on the `%jibb` desk binds it, builds `/ui` with the desk's compiler and serves it. Install `%jibb` first; until then the tile opens Eyre's 404. The app's agent, `/app/my-app.hoon`, is unchanged.
516+
517+## Sources
518+
519+- [Marks](https://docs.urbit.org/urbit-os/kernel/clay/marks) and [fas runes](https://docs.urbit.org/hoon/rune/fas), docs.urbit.org
520+- [Sail](https://docs.urbit.org/hoon/sail) and [Udon](https://docs.urbit.org/hoon/udon), docs.urbit.org
521+- [Docket files](https://docs.urbit.org/build-on-urbit/userspace/dist/docket), docs.urbit.org
522+- [urbit-os v2.139 release: Eyre cache and noun channels](https://github.com/urbit/urbit/releases/tag/urbit-os-v2.139); [eyre: add cache, PR #5927](https://github.com/urbit/urbit/pull/5927); [eyre: noun channels, PR #6472](https://github.com/urbit/urbit/pull/6472)
523+- [Publication caches for Eyre](https://gist.github.com/Fang-/1939a5b51fb2f821d960e05b74ad5f9f), an earlier design proposal; Eyre shipped `%set-response` instead
524+- [Mast](https://github.com/R-JG/mast), [Fang-/suite (Rudder)](https://github.com/Fang-/suite)
525+- [Elm architecture](https://guide.elm-lang.org/architecture/), [effects](https://guide.elm-lang.org/effects/) and [keyed nodes](https://guide.elm-lang.org/optimization/keyed.html); [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/)
526+- [elm-pages frozen views](https://elm-pages.com/docs/frozen-views/), [Astro islands](https://docs.astro.build/en/concepts/islands/), [Qwik resumability](https://qwik.dev/docs/concepts/resumable/)
527+- [@urbit/nockjs](https://github.com/urbit/nockjs), [@urbit/aura](https://github.com/urbit/aura-js)