Templates

Obsidian uses Pebble as its templating engine — a Java template engine inspired by Twig.

Basic template
<!-- views/articles/index.html -->
{% extends "layout.html" %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
    <h1>{{ title }}</h1>

    {% for article in articles %}
    <article>
        <h2>{{ article.title }}</h2>
        <p>{{ article.content }}</p>
    </article>
    {% endfor %}
{% endblock %}
Render from a controller
return render("articles/index.html", Map.of(
    "title", "My articles",
    "articles", articles
));
Layout inheritance

Define a base layout and extend it in every view.

<!-- views/layout.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My App{% endblock %}</title>
</head>
<body>
    <nav>...</nav>

    {% block content %}{% endblock %}

    {{ flash() | raw }}
    {{ flow_scripts | raw }}
</body>
</html>
Obsidian helpers

These functions are available in all Pebble templates automatically — no import needed.

CSRF
<form method="POST" action="/articles">
    {{ csrf_field() | raw }}
    <input type="text" name="title">
    <button type="submit">Create</button>
</form>

<!-- Or just the token for AJAX -->
<meta name="csrf-token" content="{{ csrf_token() }}">
Named routes
<a href="{{ route(name='articles.index') }}">All articles</a>
<a href="{{ route(name='articles.show', params={'id': article.id}) }}">View</a>
<form action="{{ route(name='articles.store') }}" method="post">...</form>
Flash messages
{{ flash() }}

Renders the flash notification if one exists. Place it in your base layout. See Flash messages.

Validation errors & old input
<input
    type="text"
    name="username"
    value="{{ old('username') }}"
    class="{{ error('username') ? 'border-red-500' : '' }}"
>
{% if error('username') %}
    <p class="text-red-400 text-sm">{{ error('username') }}</p>
{% endif %}
Scripts
{{ flow_scripts | raw }}           <!-- Client-side navigation -->

Include these at the bottom of your base layout. The | raw filter is required — without it Pebble escapes the script tags.

Common Pebble syntax
<!-- Variables -->
{{ variable }}
{{ object.property }}

<!-- Conditions -->
{% if user != null %}
    Hello, {{ user.username }}!
{% else %}
    Hello, guest!
{% endif %}

<!-- Loops -->
{% for item in items %}
    <li>{{ item.name }}</li>
{% else %}
    <li>No items found.</li>
{% endfor %}

<!-- Filters -->
{{ title | upper }}
{{ content | truncate(100) }}
{{ date | date("dd/MM/yyyy") }}
💡 Full Pebble documentation

For the complete list of tags, filters, and functions, refer to the official Pebble documentation →