Template#
Bialet renders pages using Inline HTML Strings — HTML written directly inside
your Wren .wren files. No template engine, no separate files, no build step.
Write a .wren file, return an HTML string, and that’s your page.
View: Inline HTML Strings#
Inline HTML Strings are delimited by angle brackets < and >. The string
must begin and end with the same tag. Tag names must be lowercase, start with a
letter, and contain only letters and numbers.
// A simple HTML string assigned to a variable
var str = <p>Hello World</p>
Interpolation#
Use {{ }} to embed any Wren expression inside HTML. The expression is
evaluated and its result is inserted into the string.
var name = "John"
var greeting = <h1>Hello, {{ name }}!</h1>
// Output: <h1>Hello, John!</h1>
var sum = <p>{{ 5 + 3 }}</p>
// Output: <p>8</p>
return sends the response#
Inside a .wren file, return sends the value as the HTTP response body
and stops execution immediately. Nothing after return runs. This is how
every page in Bialet works — the last statement is a return with the HTML.
// index.wren
return <html>
<head><title>My Page</title></head>
<body>
<h1>Hello World</h1>
</body>
</html>
Attributes#
Attributes follow the same rules as regular HTML — lowercase names, quoted
values. You can interpolate values with {{ }}.
var url = "/about"
var label = "Learn more"
var link = <a href="{{ url }}" class="nav-link">{{ label }}</a>
Multi-line Inline HTML#
An inline HTML string can span as many lines as you need. The newlines inside the tags become part of the HTML output.
var card = <article>
<h2>Title</h2>
<p>{{ description.safe }}</p>
</article>
Pitfalls#
Same-tag nesting is a parse error. A tag cannot directly contain another tag with the same name:
// Wrong — parse error
var bad = <div><div>Hello</div></div>
var alsoBad = <section><section>Nested</section></section>
// Correct — use a different tag
var good = <div><section>Hello</section></div>
Mismatched tags fail. Opening and closing tags must match:
// Wrong
var bad = <div><span>Hello</div>
// Correct
var good = <div><span>Hello</span></div>
Invalid tag names. Only lowercase letters and numbers are allowed. No hyphens, underscores, or uppercase:
// Wrong
var bad1 = <custom-tag>Invalid</custom-tag>
var bad2 = <my_component>Invalid</my_component>
// Correct
var good = <span class="badge">Valid</span>
Interpolation depth is limited to 9 nested levels.
Forgetting return. Without return, the response body is empty. The page
renders as blank.
Files starting with _ or . are private — they return 403 if accessed
directly. Use them for imports and internal logic only.
Conditional Rendering with &&#
Wren’s && operator is the primary tool for conditional rendering. When the
left side is truthy, the right side is returned; otherwise the expression
evaluates empty.
Class toggling#
// filter is a query parameter string
var filter = Request.get("filter") || "all"
<a href="/" class="filter-tab {{ filter == "all" && "active" }}">All</a>
<a href="/?filter=active" class="filter-tab {{ filter == "active" && "active" }}">Active</a>
<a href="/?filter=completed" class="filter-tab {{ filter == "completed" && "active" }}">Completed</a>
When filter == "all" is true, "active" is inserted as the class value.
When false, nothing is inserted.
Conditional HTML blocks#
You can conditionally render entire chunks of HTML. The right side of && can
be an inline HTML string. The {{ }} interpolation must always appear inside
an HTML tag ({{ }} outside an HTML tag is parsed as Wren map syntax).
return <main>
{{ showClear && <form method="post" action="/clear" class="clear-form">
<button class="clear-btn">Clear completed</button>
</form> }}
</main>
return <main>
{{ tasks.count == 0 && <section class="empty-state">
<span class="empty-icon" aria-hidden="true">🎉</span>
<p class="empty-text">Nothing here yet.</p>
</section> }}
</main>
// Conditional text
<span class="stats-count">
<strong>{{ activeCount }}</strong>
task{{ activeCount != 1 && "s" }} remaining
</span>
Pitfall#
Newlines outside HTML tags produce unexpected output. The Wren code
inside {{ }} must start on the same line as {{. Newlines belong inside
the HTML string (between <tag> and </tag>), not in the surrounding Wren
code. Breaking the Wren expression with newlines can cause parsing
ambiguities and empty output.
// Wrong — Wren code spans lines outside the HTML tags
return <div>{{
showClear &&
<form method="post" action="/clear">
<button>Clear</button>
</form>
}}</div>
// Correct — Wren code starts on the same line as {{
return <div>{{ showClear && <form method="post" action="/clear">
<button>Clear</button>
</form> }}</div>
The same rule applies to ternary expressions and map — keep the Wren
expression on the same line as {{, let only the HTML string span lines.
Ternary Expressions#
Ternary operators are essential for switching attributes, classes, and entire blocks of HTML based on a condition.
Attribute switching#
<button class="{{ task.finished ? "checkbox-btn checked" : "checkbox-btn" }}"
title="{{ task.finished ? "Mark incomplete" : "Mark complete" }}"
aria-label="{{ task.finished ? "Mark incomplete" : "Mark complete" }}">
</button>
Nested ternaries for multi-value decisions#
<span class="priority-dot {{ Num.fromString(task.id.toString) % 3 == 1 ? "high" : Num.fromString(task.id.toString) % 3 == 2 ? "medium" : "low" }}"
aria-hidden="true"></span>
Ternary with HTML operands#
Each branch of a ternary can be an inline HTML string that spans multiple
lines. The {{ }} must be inside an HTML tag context:
return <main>{{ task.finished ? <span class="task-text completed">
{{ task.description.safe }}
</span> : <span class="task-text">
{{ task.description.safe }}
</span> }}</main>
Pitfall#
Deeply nested ternaries hurt readability. For complex logic, compute the value above the template and use a simple variable instead:
// Compute before the template
var taskClass = "task-text"
if (task.finished) {
taskClass = "task-text completed"
}
// Clean template
<span class="{{ taskClass }}">{{ task.description.safe }}</span>
Newlines outside HTML tags — same rule as &&:
// Wrong — Wren code broken across lines outside the HTML string
return <div>{{
task.finished ?
<span class="completed">Done</span> :
<span class="pending">Pending</span>
}}</div>
// Correct — one expression, newlines inside the HTML strings
return <div>{{ task.finished ? <span class="completed">
Done
</span> : <span class="pending">
Pending
</span> }}</div>
Iteration with map#
Wren’s map function transforms each element of a list into an HTML string.
The callback body is a single Wren expression — but the inline HTML string
inside it can span many visual lines.
Simple list#
var items = ["Apple", "Banana", "Cherry"]
var html = <ul>{{ items.map { |item| <li>{{ item }}</li> } }}</ul>
// Output: <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>
Multiline items#
The callback is one expression (|task| <li>...</li>), but the <li> inline
HTML string spans multiple lines. The Wren code stays on one line of Wren, but
the HTML inside the tags has newlines.
<ul class="task-list">
{{ tasks.map{ |task| <li class="task-item {{ task.finished && "completed" }}">
<section class="task-item-row">
<form method="post" action="/toggle" class="checkbox-form">
<input type="hidden" name="id" value="{{ task.id }}" />
<button class="{{ task.finished ? "checkbox-btn checked" : "checkbox-btn" }}"
title="{{ task.finished ? "Mark incomplete" : "Mark complete" }}"
aria-label="{{ task.finished ? "Mark incomplete" : "Mark complete" }}">
</button>
</form>
<span class="task-content">
<span class="{{ task.finished ? "task-text completed" : "task-text" }}">
<span class="priority-dot low" aria-hidden="true"></span>
{{ task.description.safe }}
</span>
<span class="task-meta">{{ task.createdAt.hh }}:{{ task.createdAt.mi }}</span>
</span>
<form method="post" action="/delete" class="delete-form">
<input type="hidden" name="id" value="{{ task.id }}" />
<button class="delete-btn" title="Delete task" aria-label="Delete task">✕</button>
</form>
</section>
</li> } }}
</ul>
Pitfalls#
The callback must be a single expression. You cannot have multiple Wren
statements or variable declarations inside a map callback:
// Wrong — multiple statements inside map
{{ tasks.map { |task|
var icon = task.finished ? "✓" : "○"
<li>{{ icon }} {{ task.description }}</li>
} }}
If you need pre-computed values, prepare them before map (e.g. add computed
properties to your model class, or pre-process the list).
Empty lists produce empty output. Pair map with && to show an empty
state:
return <main>
<ul>
{{ tasks.map{ |task| <li>{{ task.description.safe }}</li> } }}
</ul>
{{ tasks.count == 0 && <section class="empty-state">
<p>No tasks yet. Add your first one above.</p>
</section> }}
</main>
Large lists hurt performance. Paginate or limit results in the controller —
map iterates everything you give it.
Newlines outside HTML tags — same rule:
// Wrong — Wren code broken across lines outside the HTML string
return <div>{{
tasks.map { |task|
<li>{{ task.description }}</li>
} }}</div>
// Correct — the expression starts on the {{ line
return <div>{{ tasks.map{ |task| <li>
{{ task.description }}
</li> } }}</div>
Bialet MVC: File Structure#
Now that you know how to write HTML, here’s how a complete .wren file is
organized. Bialet follows an MVC-like pattern within a single file:
Model — Wren classes in separate files (e.g.
_domain.wren), imported at the topController — the logic at the top of the
.wrenfile: routing decisions, POST handling, data fetching, redirectsView — the inline HTML at the bottom, passed to
returnto send the response and end the script
// === Controller ===
import "_template" for Template
import "_domain" for Post
if (Request.isPost) {
var post = Post.new()
post.title = Request.post("title")
post.save()
return Response.redirect("/")
}
var posts = Post.list()
// === View ===
return Template.new().layout(<main>
<h1>Posts</h1>
<ul>
{{ posts.map{ |p| <li><a href="/posts/{{ p.id }}">{{ p.title.safe }}</a></li> } }}
</ul>
</main>)
Pitfall#
return terminates immediately. Code after return never executes.
This means you cannot return and then run more logic. Put all controller
logic before the view.
Model: Wren Classes#
Wren classes serve as the Model layer. They encapsulate data, database queries, and domain logic — keeping it out of your template code.
// _domain.wren
class Post {
construct new(row) {
_id = row["id"]
_title = row["title"]
_body = row["body"]
_createdAt = row["createdAt"] || Date.now
}
static new() { Post.new({}) }
id { _id }
title { _title }
title=(val) { _title = val.toString.trim() }
body { _body }
body=(val) { _body = val.toString() }
createdAt { Date.new(_createdAt) }
save() { `Post`.save(this) }
static list() { `
SELECT * FROM Post ORDER BY createdAt DESC
`.fetch.to(Post) }
static find(id) { `
SELECT * FROM Post WHERE id = ?
`.first(id).to(Post) }
}
Now the controller imports this class and the view iterates over Post
instances — clean, testable, and separated from presentation.
Pitfall#
Database values come back as strings. Always convert before numeric operations:
var count = Num.fromString(row["cnt"])
// Wrong: row["cnt"] + 1 — this concatenates strings
Controller: Logic at the Top#
The top of each .wren file handles the request. This is where you:
Check the HTTP method (
Request.isPost)Read form data (
Request.post("field"))Read query parameters (
Request.get("key"))Call model methods
Redirect or prepare data for the view
import "_template" for Template
import "_domain" for Post
if (Request.isPost) {
var post = Post.new()
post.title = Request.post("title")
post.body = Request.post("body")
post.save()
return Response.redirect("/posts/" + post.id.toString)
}
var postId = Request.route(1)
var post = Post.find(postId)
Use return Response.redirect(path) to redirect after POST — this follows
the Post/Redirect/Get pattern and prevents duplicate form submissions.
Pitfall#
Forgetting return before Response.redirect() causes a double-response
error. The redirect sends headers, then the code below still executes and
tries to send a body. Always return Response.redirect(...).
// Wrong — missing return
if (Request.isPost) {
task.save()
Response.redirect("/") // redirect sends headers...
}
// ...then this still runs, trying to send a second response
return Template.new().layout(...)
Template Classes / Components#
Extract shared HTML into Wren classes. Each method returns an inline HTML string, turning your class into a library of reusable components.
Layout template#
Create _template.wren and import it from your pages:
class Template {
construct new() {
_title = "My App"
_subtitle = "A Bialet application"
}
layout(content) { <!doctype html>
<html lang="en">
{{ head(_title) }}
<body>
<header class="app-header">
<h1>{{ _title }}</h1>
<p class="app-subtitle">{{ _subtitle }}</p>
</header>
<main>{{ content }}</main>
{{ footer }}
</body>
</html> }
head(title) { <head>
<title>{{ title }}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
</head> }
footer { <footer class="app-footer">
<p>Built with <a href="https://bialet.dev">Bialet</a>.</p>
</footer> }
}
Using the template#
// index.wren
import "_template" for Template
return Template.new().layout(<main>
<h1>Welcome</h1>
<p>This content goes into {{ content }}.</p>
</main>)
A Wren block that contains a single expression implicitly returns that
expression — no return keyword needed in the method body.
Pitfalls#
Instance methods need new(). If your methods are not static, you
must instantiate the class:
// Wrong — calls static layout, doesn't exist
return Template.layout(...)
// Correct — creates an instance first
return Template.new().layout(...)
Don’t over-engineer. A template class with a handful of methods (layout,
head, footer, plus one or two component helpers) covers most needs.
Nested class hierarchies and deep abstraction layers work against Bialet’s
simplicity.
Semantic HTML#
Prefer semantic elements that describe their purpose. Bialet’s inline HTML is
plain HTML — the browser treats it exactly as if you’d written an .html file.
Use the right element for the job.
Instead of |
Use |
|---|---|
Page header / branding |
|
Navigation links |
|
Main content area |
|
Self-contained content block |
|
Thematic grouping |
|
Sidebar or complementary content |
|
Page footer |
|
A real-world page using semantic elements:
return Template.new().layout(<main>
<form method="post">
<section class="input-group">
<input name="task" placeholder="What needs to be done?" required autofocus />
<button type="submit">Add</button>
</section>
</form>
<nav class="filters" aria-label="Task filters">
<a href="/" class="filter-tab active">All</a>
<a href="/?filter=active" class="filter-tab">Active</a>
<a href="/?filter=completed" class="filter-tab">Completed</a>
</nav>
<aside class="stats-bar">
<span><strong>3</strong> tasks remaining</span>
</aside>
<article class="task-card">
<h2>Buy groceries</h2>
<p>Added 10 minutes ago</p>
</article>
<footer class="app-footer">
<p>Built with Bialet</p>
</footer>
</main>)
Semantic HTML improves accessibility (screen readers understand page structure), SEO (crawlers parse meaningful sections), and readability (future you will thank you).
Pitfall#
Same-tag nesting applies to semantic tags too.
// Wrong — parse error
var bad = <section><section>Nested section</section></section>
// Correct — use different semantic tags
var good = <section><article>Nested content</article></section>
Styling & CSS#
Bialet outputs plain HTML. CSS works exactly as it does with static HTML files. If you’re coming from React and haven’t written vanilla CSS in a while, here’s what you need to know.
Adding a stylesheet#
Place your .css file in the app root (next to your .wren files) and link it
in your template’s <head>:
head { <head>
<title>{{ _title }}</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/style.css" />
</head> }
Writing CSS#
Use CSS custom properties for theming and write class-based selectors targeting your semantic HTML elements. Here’s a minimal starting point:
/* style.css */
:root {
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #1e293b;
--accent: #14b8a6;
--radius: 14px;
}
body {
font-family: system-ui, sans-serif;
background: var(--bg);
color: var(--text);
padding: 2rem 1rem;
}
main {
max-width: 540px;
margin: 0 auto;
}
header {
margin-bottom: 2rem;
}
.card {
background: var(--card-bg);
border-radius: var(--radius);
padding: 1rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
Semantic CSS first (Bialet philosophy)#
Target elements directly by tag name where possible, then add classes for variants. This keeps both your HTML and CSS lean:
/* Base: all nav elements get a consistent look */
nav {
display: flex;
gap: 0.25rem;
background: var(--card-bg);
border-radius: var(--radius);
padding: 0.25rem;
}
/* Variant: only the filter nav gets extra styling */
nav.filters {
margin-bottom: 1.25rem;
}
PicoCSS#
PicoCSS is a classless CSS framework that styles semantic HTML elements
automatically. It pairs perfectly with Bialet’s semantic HTML approach — you
write <nav>, <main>, <article>, and PicoCSS handles the styling with zero
classes.
head { <head>
<title>{{ _title }}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
</head> }
With PicoCSS loaded, your semantic HTML gets a clean, consistent look without writing any CSS. Add a custom stylesheet alongside it for app-specific overrides.
Tailwind CSS#
Tailwind works in Bialet — it’s just classes on HTML elements. For prototyping, use the CDN script:
head { <head>
<title>{{ _title }}</title>
<meta charset="utf-8" />
<script src="https://cdn.tailwindcss.com"></script>
</head> }
Then use utility classes directly:
<main class="max-w-lg mx-auto py-8 px-4">
<h1 class="text-2xl font-bold mb-6">Dashboard</h1>
<article class="bg-white rounded-xl shadow-sm p-4">
<p class="text-gray-600">Hello World</p>
</article>
</main>
For production, self-host the compiled CSS. Run Tailwind’s CLI or PostCSS build
against the app directory — the class names in your .wren files are standard
HTML attributes, so any CSS processing tool works.
Pitfalls#
No CSS preprocessors built in. Bialet doesn’t bundle Sass, Less, or PostCSS. Write vanilla CSS, or use a separate build step for preprocessing.
_-prefixed CSS files are not served. Files starting with _ or .
return 403. Name your stylesheets without a leading underscore:
// ✓ Served
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="/theme.css" />
// ✗ Returns 403
<link rel="stylesheet" href="/_style.css" />
Framework CDNs add an external dependency. For production, download the CSS file and serve it from your app directory alongside your other static assets.
Escaping & Security#
The {{ }} interpolation does not escape HTML by default. When displaying
user-generated content, database values, or URL parameters, you must use the
.safe property to escape special HTML characters and prevent XSS attacks.
var userInput = "<script>alert('xss')</script>"
// Dangerous — XSS vulnerability
var dangerous = <p>{{ userInput }}</p>
// Renders: <p><script>alert('xss')</script></p>
// Safe — HTML characters are escaped
var safe = <p>{{ userInput.safe }}</p>
// Renders: <p><script>alert('xss')</script></p>
You can also use Util.htmlEscape() for manual escaping:
var escaped = Util.htmlEscape(userInput)
Rule: Always use .safe on any string that comes from user input, database
queries, or URL parameters before inserting it into HTML.
Pitfall#
Forgetting .safe is an XSS vulnerability. Every user-supplied string
embedded in HTML must be escaped. There is no automatic escaping.
Common Pitfalls#
Same-tag nesting:
<div><div>Hello</div></div>is a parse error — use different tag names for nesting.Mismatched tags:
<div><span>Hello</div>fails — opening and closing tags must match.Invalid tag names: Only lowercase letters and numbers — no hyphens, underscores, or uppercase. Use classes and semantic tags instead.
Interpolation depth: Maximum 9 nested
{{ }}levels.Forgetting
return: Withoutreturn, the response body is empty._/.-prefixed files: Private, return 403 if accessed directly.Newlines outside HTML tags in
{{ }}: Keep Wren code on the same line as the opening{{. Only HTML strings can span lines.Map callback is a single expression: No multiple statements, no variable declarations inside the callback.
Empty lists in
map: Produce empty output — pair with&&for empty states.returnterminates immediately: Code afterreturnnever executes.DB values are strings: Convert with
Num.fromString()before math.Forgetting
returnbeforeResponse.redirect(): Causes double-response errors.Instance vs static methods: Instance methods need
new(); static methods don’t._-prefixed CSS is not served: Name stylesheets without leading_.CSS preprocessors not built in: Write vanilla CSS or use a build step.
Framework CDNs in production: Self-host CSS files for reliability.
{{ }}does not escape HTML: Always use.safeon untrusted strings.