` body in `` for the runtime | [`reference/crates/wo-htmlx/src/render.rs`](../../../reference/crates/wo-htmlx/src/render.rs) (140 LOC) — extend by ~70 LOC |
| `manifest.rs` | Walks the AST, collects subscriptions + bind sites, serialises JSON | new (~150 LOC) |
Total: ~835 LOC (585 ported + ~250 new).
### `Cargo.toml` change
```toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```
(Both already in `crates/rt`; `crates/ui` adopts them rather than hand-rolling JSON for the manifest at this phase.)
### Manifest schema
```json
{
"version": 1,
"subscriptions": [
{
"id": "orders-live-0",
"source": "Order{ status != Cancelled }",
"key": "id",
"sort": "placed_at desc",
"filter": null,
"root_selector": "[data-wo-subscription=\"orders-live-0\"]"
}
],
"bind_sites": [
{ "subscription_id": "orders-live-0", "key": "id", "field": "status" },
{ "subscription_id": "orders-live-0", "key": "id", "field": "total" }
]
}
```
## API shape (target)
```rust
use ui::htmlx::{Template, Manifest, HelperRegistry};
let tmpl = Template::parse(src)?; // Result
let html = tmpl.render(&ctx, ®istry)?; // Result
let mani = tmpl.manifest(); // Manifest
// SSR pattern: page = head + html + ""
```
## Exit criteria
1. `cargo build -p ui` green; no new top-level dependencies beyond `serde`/`serde_json`.
2. **Golden parse + render** for every `.htmlx` file under [`docs/examples/blog/ui/components/`](../../examples/blog/ui/components/) and [`docs/examples/ecommerce/shared/components/`](../../examples/ecommerce/shared/components/) — output is HTML and parses back into an isomorphic AST.
3. **Manifest emission** for `…` produces a `LiveSubscription` with the source string preserved verbatim and the body wrapped under `data-wo-subscription="orders-live-0"`.
4. **Bind-site collection** for `| {{status}} | ` inside `` records `(subscription_id, key="id", field="status")` once and only once.
5. **v1 regression**: `cargo run --bin wo -- run docs/examples/blog` continues to start without parser errors. The blog sample has no `` or `wo:bind` today; nothing should regress.
6. All 14 existing `crates/rt` tests pass.
## Non-scope
- **No SSR.** Phase 02 emits templates; phase 03 ships the runtime; serving them is a downstream concern that the per-app binary in phase 05 wires together.
- **No nested ``.** Parse error in this phase. Author can compose live subtrees by partial inclusion (`{{> child}}`).
- **No author-extensible helpers.** The closed enum is the contract for this phase.
- **No streaming render.** Templates are rendered to a single `String`.
- **No manifest version negotiation.** Wire format is `version: 1` always.
## Verification
```bash
cargo build -p ui
cargo test -p ui --test golden_v1 # blog/ecommerce templates byte-identical
cargo test -p ui --test golden_extensions # + wo:bind cases
cargo test -p ui --test manifest # manifest emission
# v1 regression
cargo run --bin wo -- run docs/examples/blog &
PID=$!; sleep 1; curl -fsS http://127.0.0.1:8080/ >/dev/null; kill $PID
cd reference/crates && cargo build && cargo test
```
## After this phase
Phase 02 (`02-ui-compiler.md`) consumes the `Template` + `Manifest` types defined here as its emission target — every `##ui` block compiles down to an `.htmlx` file that parses cleanly under this phase's parser. Phase 03 (`03-client-runtime.md`) consumes the manifest JSON schema as its wire input.