Views & Templates
Views handle the presentation layer of your application. Soli uses a familiar, expressive template syntax that combines HTML with dynamic logic.
ERB Syntax
Soli uses ERB-style tags. Use <%= ... %> to output values, and <% ... %> for logic like loops or conditionals.
Template Syntax
Output Variables
<!-- Basic variable output -->
<h1><%= title %></h1>
<p>Hello, <%= name %>!</p>
<!-- Accessing hash data -->
<p>User: <%= user["name"] %></p>
<!-- Expressions -->
<p>Total: $<%= price * quantity %></p>
Control Flow
<!-- Conditionals -->
<% if user_logged_in %>
<span>Welcome back!</span>
<% else %>
<a href="/login">Login</a>
<% end %>
<!-- Loops -->
<ul>
<% for post in posts %>
<li><%= post["title"] %></li>
<% end %>
</ul>
Template Helper Functions
These helper functions are automatically available in all templates.
DateTime Functions
<!-- Get current timestamp -->
<%= datetime_now() %>
<!-- Format a timestamp with strftime -->
<%= datetime_format(post["created_at"], "%Y-%m-%d") %>
<%= datetime_format(post["created_at"], "%B %d, %Y") %>
<%= datetime_format(datetime_now(), "%A, %B %d, %Y at %H:%M") %>
<!-- Parse a date string to timestamp -->
<%= datetime_parse("2024-01-15") %>
<!-- Add/subtract time -->
<%= datetime_add_days(datetime_now(), 7) %> <!-- 7 days from now -->
<%= datetime_add_days(datetime_now(), -30) %> <!-- 30 days ago -->
<%= datetime_add_hours(datetime_now(), 2) %> <!-- 2 hours from now -->
<!-- Human-readable relative time -->
<%= time_ago(post["created_at"]) %> <!-- "5 minutes ago", "2 hours ago", etc. -->
<%= time_ago(post["updated_at"]) %>
<!-- Difference in seconds -->
<%= datetime_diff(start_time, end_time) %>
<!-- Localized date formatting (uses current I18n locale) -->
<%= l(post["created_at"]) %> <!-- short format: "01/15/2024" -->
<%= l(post["created_at"], "long") %> <!-- "January 15, 2024" -->
<%= l(post["created_at"], "full") %> <!-- "Monday, January 15, 2024" -->
<%= l(post["created_at"], "time") %> <!-- "10:30 AM" -->
<%= l(post["created_at"], "datetime") %> <!-- "01/15/2024 10:30 AM" -->
<%= l(post["created_at"], "%Y-%m-%d") %> <!-- custom strftime -->
| Function | Description |
|---|---|
| datetime_now() | Returns current Unix timestamp (UTC) |
| datetime_format(ts, fmt) | Format timestamp with strftime (e.g., "%Y-%m-%d", "%B %d, %Y") |
| datetime_parse(str) | Parse date string to timestamp (ISO 8601, RFC 3339) |
| datetime_add_days(ts, n) | Add n days to timestamp (negative to subtract) |
| datetime_add_hours(ts, n) | Add n hours to timestamp |
| datetime_diff(t1, t2) | Difference between timestamps in seconds (t1 - t2) |
| time_ago(ts) | Human-readable relative time ("2 hours ago", "3 days ago") |
| l(ts, format?) | Localized date format using current locale ("short", "long", "full", "time", "datetime", or strftime) |
Strftime Format Codes
Use these codes with datetime_format() or l() for custom formatting:
| Code | Description | Example |
|---|---|---|
| %Y | 4-digit year | 2024 |
| %y | 2-digit year | 24 |
| %m | Month (01-12) | 01 |
| %B | Full month name | January |
| %b | Abbreviated month | Jan |
| %d | Day of month (01-31) | 15 |
| %e | Day of month (space-padded) | 5 |
| %A | Full weekday name | Monday |
| %a | Abbreviated weekday | Mon |
| %H | Hour 24h (00-23) | 14 |
| %I | Hour 12h (01-12) | 02 |
| %M | Minute (00-59) | 30 |
| %S | Second (00-59) | 45 |
| %p | AM/PM | PM |
| %Z | Timezone name | UTC |
| %j | Day of year (001-366) | 015 |
| %W | Week number (00-53) | 03 |
| %% | Literal % | % |
<!-- ISO format -->
<%= datetime_format(ts, "%Y-%m-%d") %> <!-- 2024-01-15 -->
<%= datetime_format(ts, "%Y-%m-%dT%H:%M:%S") %> <!-- 2024-01-15T14:30:45 -->
<!-- Human-readable -->
<%= datetime_format(ts, "%B %d, %Y") %> <!-- January 15, 2024 -->
<%= datetime_format(ts, "%A, %B %e, %Y") %> <!-- Monday, January 15, 2024 -->
<!-- Time formats -->
<%= datetime_format(ts, "%H:%M") %> <!-- 14:30 (24h) -->
<%= datetime_format(ts, "%I:%M %p") %> <!-- 02:30 PM (12h) -->
<!-- Combined -->
<%= datetime_format(ts, "%b %d at %I:%M %p") %> <!-- Jan 15 at 02:30 PM -->
I18n Functions
<!-- Get current locale -->
<%= locale() %> <!-- "en", "fr", etc. -->
<!-- Set locale (usually done in controller) -->
<% set_locale("fr") %>
<!-- Translate a key -->
<%= t("hello") %>
<!-- Translate with fallback -->
<%= t("greeting", "Welcome!") %>
| Function | Description |
|---|---|
| locale() | Get current locale code (e.g., "en", "fr") |
| set_locale(code) | Set the current locale |
| t(key, fallback?) | Translate a key with optional fallback |
HTML Functions
<!-- HTML escaping (prevent XSS) -->
<%= html_escape(user_input) %>
<%= h(user_input) %> <!-- shorthand -->
<!-- Strip HTML tags -->
<%= strip_html(post["content"]) %>
<!-- Sanitize HTML (remove dangerous tags/attributes) -->
<%= sanitize_html(user_content) %>
<!-- Unescape HTML entities -->
<%= html_unescape("<p>") %>
<!-- Substring (useful for truncating) -->
<%= substring(post["content"], 0, 100) %>...
Utility Functions
<!-- Generate a range for loops -->
<% for i in range(1, 5) %>
<p>Item <%= i %></p>
<% end %>
<!-- Asset paths with cache busting -->
<link href="<%= public_path("css/app.css") %>" rel="stylesheet">
<script src="<%= public_path("js/app.js") %>"></script>
<!-- Output: /css/app.css?v=a1b2c3... -->
<!-- String concatenation -->
<%= "Hello, " + name + "!" %>
<%= "Total: $" + price * quantity %>
Application Helpers
When you create a new app with soli new, a starter helper file is generated at app/helpers/application_helper.soli. These helpers are automatically available in all templates.
Text Helpers
<!-- Truncate text with ellipsis -->
<%= truncate(post["content"], 100) %>
<!-- "This is a very long article that..." -->
<%= truncate(title, 50, " [more]") %>
<!-- "This is a long title that gets cut [more]" -->
<!-- Capitalize first letter -->
<%= capitalize("hello world") %>
<!-- "Hello world" -->
<%= capitalize(user["status"]) %>
<!-- "Active" (if status was "active") -->
<!-- Pluralize based on count -->
<%= pluralize(1, "item") %>
<!-- "1 item" -->
<%= pluralize(5, "item") %>
<!-- "5 items" -->
<%= pluralize(count, "person", "people") %>
<!-- "1 person" or "3 people" -->
Number & Currency Helpers (I18n)
<!-- Format numbers with thousands separator (auto-detects from locale) -->
<%= number_with_delimiter(1234567) %>
<!-- en: "1,234,567" | fr: "1 234 567" | de: "1.234.567" -->
<!-- Override delimiter manually -->
<%= number_with_delimiter(1234567, "'") %>
<!-- "1'234'567" -->
<!-- Format as currency (locale-aware: symbol, delimiter, position) -->
<%= currency(1000) %>
<!-- en: "$1,000" | fr: "1 000 €" | de: "1.000 €" | ja: "¥1,000" -->
<%= currency(order["total"]) %>
<!-- Override symbol manually -->
<%= currency(1234, "£") %>
<!-- "£1,234" -->
Locale-Aware Formatting
number_with_delimiter() and currency() automatically use the current I18n locale.
Use set_locale("fr") in your controller to change formatting.
Supported: en, fr, de, es, it, pt, ja, zh, ru.
Link Helper
<!-- Generate safe HTML links (XSS protected) -->
<%= link_to("Home", "/") %>
<!-- <a href="/">Home</a> -->
<%= link_to("View Profile", "/users/" + user["id"]) %>
<!-- <a href="/users/123">View Profile</a> -->
<%= link_to("Edit", "/posts/" + post["id"] + "/edit", "btn btn-primary") %>
<!-- <a href="/posts/456/edit" class="btn btn-primary">Edit</a> -->
URL Slug Helper
<!-- Convert text to URL-friendly slug -->
<%= slugify("Hello World!") %>
<!-- "hello-world" -->
<%= slugify("My Blog Post Title") %>
<!-- "my-blog-post-title" -->
<%= slugify("Café & Restaurant") %>
<!-- "cafe-restaurant" -->
<!-- Use in URLs -->
<a href="/posts/<%= slugify(post["title"]) %>">
<%= post["title"] %>
</a>
| Function | Description |
|---|---|
| truncate(text, length, suffix?) | Truncate text to length with suffix (default: "...") |
| capitalize(text) | Capitalize first letter of string |
| pluralize(count, singular, plural?) | Pluralize word based on count (default plural: singular + "s") |
| number_with_delimiter(num, delim?) | Format number with thousands separator (locale-aware) |
| currency(amount, symbol?) | Format as currency (locale-aware: symbol, delimiter, position) |
| link_to(text, url, class?) | Generate HTML link with XSS protection |
| slugify(text) | Convert text to URL-friendly slug |
Complete Example
<article class="post">
<h1><%= post["title"] %></h1>
<div class="meta">
<!-- Localized date (respects I18n.set_locale) -->
<span>Published: <%= l(post["created_at"], "long") %></span>
<span>(<%= time_ago(post["created_at"]) %>)</span>
</div>
<% if post["updated_at"] != post["created_at"] %>
<p class="updated">Last updated: <%= time_ago(post["updated_at"]) %></p>
<% end %>
<div class="content">
<%= truncate(post["content"], 500) %>
</div>
<div class="stats">
<span><%= number_with_delimiter(post["views"]) %> views</span>
<span><%= pluralize(post["comment_count"], "comment") %></span>
</div>
<div class="actions">
<%= link_to("Edit", "/posts/" + post["id"] + "/edit", "btn btn-secondary") %>
<%= link_to("Back to Posts", "/posts", "btn btn-link") %>
</div>
<footer>
<p>© <%= datetime_format(datetime_now(), "%Y") %> My Blog</p>
</footer>
</article>
Layouts
Layouts define the outer shell of your pages. Use <%= yield %> to inject the view content.
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<nav>...</nav>
<main>
<%= yield %>
</main>
</body>
</html>
Partials
Reuse components across views. Partials are named with a leading underscore.
<!-- Render _user_card.html.erb -->
<%= render_partial("partials/user_card", { "user": user }) %>
Passing Data
Pass a hash of data from your controller to the view.
fn show(req) {
let post = Post.find(req.params["id"]);
return render("posts/show", {
"title": post["title"],
"post": post
});
}
Security Warning
Always use h() or html_escape() when outputting user-generated content to prevent XSS attacks.
Soli escapes output in <%= %> by default, but be cautious with raw output.
Best Practices
- Keep logic out of views. If it's complex, it belongs in a helper or controller.
- Use partials for small, reusable components like buttons, cards, or alerts.
- Organize views into folders matching your controller names.