ESC
Type to search...
S
Soli Docs

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

template.html.erb
<!-- 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

template.html.erb
<!-- 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

DateTime helpers
<!-- 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 % %
Common format examples
<!-- 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

I18n helpers
<!-- 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 helpers
<!-- 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("&lt;p&gt;") %>

<!-- Substring (useful for truncating) -->
<%= substring(post["content"], 0, 100) %>...

Utility Functions

Utility helpers
<!-- 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

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)

Number helpers (locale-aware)
<!-- 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

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

Slugify 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

app/views/posts/show.html.erb
<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>&copy; <%= 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.

app/views/layouts/application.html.erb
<!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.

Usage
<!-- 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.

app/controllers/posts_controller.soli
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.

Next Steps