writeonce/docs/examples/wo-html/README.md
shoney.arickathil 3afdafa5c4 feat(site): logo, favicon, install guide, package catalogue
- layout/logo.wo: the mark as inline SVG — dark tile, two-stroke "W"
  (white then accent blue). One source: nav brand + /favicon.svg
- favicon/controller.wo: GET /favicon.svg, image/svg+xml, day cache
- install/: GET /install — toolchain tarball, PATH, verify, first
  project, build/run, adding a dep. Copy from the real install README
- packages/: GET /packages + /packages/:name — catalogue with the
  [deps] line, what each library gives you, and a usage snippet.
  Index cards are child components (multi Component)
- wo-html: page_head(title, head, body) and a `head` slot on Layout —
  a favicon link or meta tag had nowhere else to go; page() passes ""
- header: Install/Tutorial/Packages/GitHub, brand shows the mark
- main.wo: SITE_HOST picks the interface (loopback default), bound
  address printed at startup
- site-accept: 11 -> 17 checks (install, packages x2, 404, favicon,
  inline logo)

Verified on 192.168.0.165:8080 — every route, favicon bytes, and the
mark rasterised at 256px and 32px.

Gates: oop-accept MET, site 17/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 04:05:00 +02:00

105 lines
4.4 KiB
Markdown

# wo-html — server-rendered HTML as plain Text
A view library, not a framework and not a template engine. Everything in
it is a pure function or a class with a `render()`; nothing here opens a
socket, reads a file, or touches the database.
```
[deps]
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
```
## The four layers
| layer | what it is |
| --- | --- |
| `esc()` | HTML-escape `& < > "`. A `{{ }}` hole in a raw text literal compiles to a call to this, so display data is escaped by construction; calling it by hand is the fallback, not the norm |
| `el()`, `link()`, `card()`, `form_post()`, … | element builders — `el("h1", "text-3xl font-bold", t)` |
| `Component` / `Layout` / `render_all()` | the view unit and its composition |
| `tw_css()` / `page()` | a hand-written Tailwind-style utility sheet, inlined into one self-contained document — no CDN, no build step, no JS |
## Components
A component is a class with fields and `fn render() -> Text`. Nothing
declares that it implements `Component` — satisfaction is **structural**,
exactly like the framework's `Handler`. Its fields ARE its inputs; the
language has no closures, so a field is the capture.
```
class ProductCard {
sku: Text
name: Text
fn render() -> Text {
return `
<div class="card">
<h3><a href="/p/{{ self.sku }}">{{ self.name }}</a></h3>
</div>`;
}
}
```
Composition is nesting — a parent holds children and calls their render:
```
pub class ProductListPage {
cards: multi Component
fn render() -> Text {
return `<div class="grid">${render_all(self.cards)}</div>`;
}
}
```
`multi Component` holds a heterogeneous list **directly**; no wrapper
record is needed (the framework's `Mw`/`Aw` wrappers are not a language
requirement). `render_all(cs)` renders children in order.
`Layout { title, head, nav, content, footer }` is content projection —
Angular's `<ng-content>` with the slots as ordinary pre-rendered `Text`.
The caller passes `child.render()`, a raw literal, or a builder's output;
the layout never learns which, which is precisely why it never needs the
child's type. A page wanting a fixed-width column wraps its content
before handing it over — deliberately no container knob here. `head` is
the one slot that is not body markup: a favicon link or a meta tag has
nowhere else to go, and `""` is the ordinary value (`page()` passes it
for you).
`Layout` renders through `page()`, so it inlines the utility sheet. An
app that links a real stylesheet instead writes its own two-slot shell
component — `docs/examples/shop/layout/app.wo` is that case, and it is a
component like any other.
## The MVC seam
| | where it lives |
| --- | --- |
| **Model** | `@table` rows, queried in the HANDLER. This library contains no `from … select` anywhere and must not grow one |
| **View** | components: fields in, Text out. No hidden state, no globals — a page is byte-deterministic from its fields |
| **Controller** | the framework's `Handler`: it queries, fills the component's fields, and answers `ok_html(c.render())` |
`ok_html` is the **framework's** (`framework/http`, beside `ok_text` and
`ok_json`): a status line plus a content-type is transport, not
rendering, so wo-html never learns what a `Resp` is.
The seam is what makes a view testable without a server and a query
testable without markup. Breaking it looks like one convenience — a
component that queries "just this once" — and costs both.
## Deliberately absent
Client-side anything (change detection, event bindings, two-way binding,
SPA routing, hydration): the no-JS posture stands, and interactivity is
form round trips. A runtime template engine: reflection-free means an
untyped `map<Text, Text>`, so templates compile to code at build time or
they do not exist. Dependency injection: components are data-in,
Text-out. Structural directives (`w:if` / `w:for`): the language's own
`if` and `for` compose literals, and no sample has yet proven the need
for a second control-flow dialect. Scoped CSS: the utility sheet stays
one static string until the pain is measured.
## Consumers
- [`docs/examples/site`](../site) — the tutorial site: `Layout` +
a `ChapterNav` component reused on the homepage and every chapter page.
Gated by `just site`.
- [`docs/examples/shop`](../shop/README.md) — the program template: its
own `AppShell` component, page components holding child components.