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>

Self-Closing Tags#

Certain HTML tags are self-closing. In Bialet, these must include a space before the closing slash. The final output omits the slash.

  • Correct: <hr />, <br />, <input value="{{ val }}" />, <meta charset="utf-8" />

  • Incorrect: <hr/>, <br/>

var inputField = <input value="{{ userInput }}" />
// Renders: <input value='Hello'>

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 top

  • Controller — the logic at the top of the .wren file: routing decisions, POST handling, data fetching, redirects

  • View — the inline HTML at the bottom, passed to return to 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:

  1. Check the HTTP method (Request.isPost)

  2. Read form data (Request.post("field"))

  3. Read query parameters (Request.get("key"))

  4. Call model methods

  5. 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 <div>

Use

Page header / branding

<header>

Navigation links

<nav>

Main content area

<main>

Self-contained content block

<article>

Thematic grouping

<section>

Sidebar or complementary content

<aside>

Page footer

<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>&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;</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: Without return, 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.

  • return terminates immediately: Code after return never executes.

  • DB values are strings: Convert with Num.fromString() before math.

  • Forgetting return before Response.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 .safe on untrusted strings.