The unified idiom¶
Experimental — opt in per mount
The unified idiom is a working spike, enabled with mount_component(..., unified=True) and proven by the /mail demo + its e2e. The surface may still change (the ~ glyph in particular is under review). The base dialect is not going anywhere.
The problem it removes¶
In the base dialect you hand-sort every template token by where it executes: Jinja {{ t.subject }} (server, frozen) one attribute away from :text="count" (client, live). That execution-location axis is exactly the thing situ derives everywhere else. The axis that deserves your attention is different: what crosses the wire, and what is live — because that governs payload, exposure, and latency.
The unified idiom dissolves the first axis and makes the second legible — in plain source text, no editor tooling required.
One interpolation¶
<span>filtering: { search }</span> <!-- depends on a Local → live client binder -->
<button>{ t.sender } — { t.subject }</button> <!-- row/server state → server-rendered -->
Write { expr } everywhere. The compiler reads the expression's dependencies and their sites: touching any Local/Synced signal → a live binder; touching only server/url/row state → server-rendered. You never choose {{ }} vs :text again. ({ } is JSX/Svelte's glyph, picked because Jinja owns {{ }}.)
The wire projection¶
<ul :each="t in mailbox ~ [id, subject, sender, unread]">
<li :show="search.lower() in t.subject.lower()"
:class="open: open_id == t.id">
<button @click="open_id = t.id; open_msg(t.id)">
{ t.sender } — { t.subject } { "●" if t.unread else "" }
</button>
</li>
</ul>
<div data-region class="reading-pane">
{% if open_message %}<pre>{ open_message.body }</pre>{% endif %}
</div>
The ~ [...] clause is the complete list of per-row fields allowed to cross to the browser. The desugarer emits the server loop with data-* attributes for exactly those fields — and referencing anything else is a build failure (real output):
CompileError: t.body is referenced in the :each over 'mailbox', but 'body' is
not in its client projection ~[id, subject]. It never crosses to the browser.
Add it to the projection (it will be shipped), or load it on demand via a
server read into a data-region.
So the classic leak — render every message body and CSS-hide all but one — is impossible to compile. The mail demo browser-verifies the contract: no body text in the initial DOM, client search filters with zero requests, and opening a message fetches exactly one body via the open_msg command.
Why this is worth a new construct¶
The projection is written at the data source, so wire cost is readable off the template; it doubles as a per-row schema (the fields and, eventually, their types — the groundwork for derived typed templates); and it turns an OWASP-catalogued defect class (excessive data exposure through over-rendering) into a compile error. It is the one construct in situ's surface with no template-language ancestor — the lineage is SQL's π and GraphQL selection sets, moved into the template.
Current limits¶
- Conditionals are still Jinja
{% if %}(a site-derived:ifis the designed next step). - The interpolation translator covers the common expression subset and fails closed on the rest.
- A client interpolation SSRs empty and fills at boot (no first-paint value yet).
- A purely server-rendered list (no client binders inside) is ordinary SSR and needs no projection.