Template Syntax
Caspian's authored syntax is split across two runtimes: Jinja renders on the server, and PulsePoint enhances the final HTML in the browser. Caspian's current syntax contract is HTML-first.
Server-Side Jinja
Use native Jinja in authored route, layout, and component HTML whenever the value is known on the server. That includes text interpolation, attribute values, and normal control flow.
<h1>Welcome, {{ user.name }}</h1> <div class="{{ 'hidden' if not is_visible else 'block' }}"> Server-visible content </div> {% if user.is_admin %} <p>Admin tools</p> {% else %} <p>Standard dashboard</p> {% endif %} {% for post in posts %} <article>{{ post.title }}</article> {% endfor %}
page(),
layout(),
metadata, auth state, or other server-prepared context. In current
authored templates, Jinja blocks are the default control-flow syntax.
Browser-Side PulsePoint
Use PulsePoint when the browser owns the value after first render. That means interactive text, event handlers, list updates, refs, effects, and RPC-driven UI state.
<section> <h2>title</h2> <p>Count: count</p> <button onclick="setCount(count + 1)"> Increment </button> <template pp-for="item in items"> <div key="item.id">item.label</div> </template> <script> const title, items = [] = pp.props; const [count, setCount] = pp.state(0); </script> </section>
Use PulsePoint for
- Inline events such as
onclick,oninput, andonsubmit. pp.state(...),pp.effect(...),pp.ref(...), andpp.rpc(...).- Client-owned template expressions like
countand list rendering withpp-for.
Hard invariant
Author a plain <script>
inside the template it belongs to. Do not handwrite
pp-component
or type="text/pp".
Caspian injects those at render time.
Choose The Right Syntax
{{ user.name }} {% if user.is_admin %} {% for item in items %}
count onclick="save()" pp-for="item in items"
Controlled And Uncontrolled Fields
A form control is controlled or uncontrolled for the lifetime of the
element, and the spelling you choose picks the mode. Bind
value or checked
for a controlled field. Use the lowercase
defaultvalue and
defaultchecked attributes to seed an
uncontrolled one.
<!-- controlled: PulsePoint owns the value --> <input value="email" oninput="setEmail(event.target.value)" /> <input type="checkbox" checked="agreed" /> <!-- uncontrolled: the DOM owns the value after first paint --> <input defaultvalue="user.email" name="email" /> <input type="checkbox" defaultchecked="user.subscribed" />
Do not mix the two
Binding value
to state that starts as undefined
flips the mode mid-life and logs a one-time
[PP-WARN]
about an element changing from uncontrolled to controlled. Fix the initial
state rather than adding both attributes.
Lowercase only
defaultvalue and
defaultchecked
are real PulsePoint syntax. The camelCase React spellings
defaultValue and
defaultChecked
are not, and are ignored.
Native HTML Attributes
In authored route and layout HTML, prefer normal HTML attribute names:
class, for,
data-*, aria-*,
and lowercase event attributes like onclick.
| Preferred in authored HTML | Avoid teaching as the default |
|---|---|
| class="card" | className="card" |
| for="email" | htmlFor="email" |
| data-test-id="save-button" | dataTestId="save-button" |
| onclick="save()" | onClick="save()" |