Pebble Templates

Alpas uses Pebble as its templating engine. Pebble is a modern, powerful templating engine with great features such as Template inheritance (layout, extends, partials etc.), Macros, built-in auto-escaping security, rich set of built-in tags, filters, and functions, etc.

It also very straightforward to extend the engine with your own filters, tags, and functions.

Pebble is a sub-set of Twig and is very similar to Jinja templating engine. If you already know Twig, or even Jinja, you already know most of Pebble's syntax. You can see the similarities and differences between both engines in this compatibility matrix.

Alpas enriches already powerful Pebble with some of its own extensions. This also means, for better or worse, Pebble is the only supported templating engine out-of-the-box and has is tightly integrated with the core of Alpas.

While it is certainly possible to use some other templating engines, when it comes to power, features, simplicity, flexibility, and support, Pebble is the best. We really believe that once you have ascended the initial learning curve of Pebble, which isn't really steep to begin with, you'll never go back to any other templating engines. There is a good reason why we went with Pebble after considering many other options, including dumping our own in-house built templating engine (may it rest in peace)!

Templates Location

Templates must be kept in the resources/templates folder and, by default, must end with .peb extension. If you wish, you can change the default extension by overriding templateExtension property of dev.alpas.view.ViewConfig class.

/info/ For performance boost, templates are cached during production but not during development. Any data that you pass to a template are not cached. So you can render the same template for different requests without worrying about either the performance or stale data.

Global Variables

If you want some variables to be globally available to all your templates, you can override getGlobalVariables() method in your custom extension and return a Map<String, Any>. This map will be merged into the data passed from a controller to a template when rendering.

Auto Reloading Template Changes

When you are tweaking and making changes to a template, you may want your new changes to be visible immediately, and, preferably, have the browser reload the changes for you as you make changes. This facilitates rapid development experience without having to recompile everything and switch back-and-forth between the browser and your IDE.

If it isn't already set up when you first initialized your project, it's easy to enable this by using the link:templates Alpas command.

$ alpas link:templates

Once the link is created, you can run the app, make some changes in one of your templates, switch to the browser, and refresh. If you want even a better and more rapid development experience, combine this with the Watching Changes and Auto Reloading.

The Template

Here is an example of what a typical layout template looks like in Alpas.

<!doctype html>
<html lang="en">
  <meta charset="utf-8">
  <link rel="stylesheet" href="{{ mix('css/app.css') }}">
  <title> {{ env('APP_NAME') }} - {{ block('title') }}</title>
  <script src="{{ mix('js/app.js') }}" defer></script>


<div id="app">
    {% if hasFlash('success') %}
          <div class="text-green">{{ flash('success') }}</div>
    {% endif %}

    {% block content %} {% endblock %}


And, this is what a child template looks like:

{% extends "layout/app.twig" %}
{% block title %} Welcome Page  {% endblock %}

{% block content %}
    <h1>Hello, {{ name }}!</h1>
{% endblock %}

You can render the above template from within a controller by calling render() method and passing just the name of the template without the extension.

fun index(call: HttpCall) {
    // you only need to pass the name of the template without its extension
    call.render("index", mapOf("name" to "Alpas"))

Nested Templates

To better organize or to group them logically, templates can be nested in sub-directories. In fact, we highly encourage you to organize your templates with sub-directories. There are no performance penalties for using sub-directories. When rendering these nested templates, you need to reference them using either a / "slash" notation (recommended), or using a . "dot" notation.

Let's say we have a template resources/templates/docs/latest/toc.peb. You can reference this template from a controller like so:

call.render("docs/latest/toc") Or, alternatively, call.render("docs.latest.toc").

/tip/ During development, you will be tweaking your templates and changing the data that get passed from a controller a lot. If you are using IntelliJ IDEA, instead of recompiling and re-running your app again and again, you can just reload your changed classes from Run > Reload Changed Classes menu and then switch to your browser and reload the page. Even better - set a shortcut for this menu like CMD+R. This is a huge time saver and productivity booster!

Data Access

Context data passed from a controller

A controller can pass the data to a template as a map of string, and an any object. The any object could be a nested map as well. Inside a template you can access each value inside the map using its key. A nested map's values can be accessed using nested . "dot" syntax.

// in your controller:
val address = mapOf("street" to "123 Broadway Ave", "zip" to "123456") 
call.render("index", mapOf("name" to "Jane", "address" to address))

<-- in your template -->
<h1>Hello, {{ name }}!</h1>

<h2>Your address:</h2>
    Street: {{ address.street }}
    Zip: {{ }}  

Sharing view data on HttpCall object

A handy method, aptly named share(pair: Pair<String, Any?>, vararg pairs: Pair<String, Any>), can be used to share some data with the templates from anywhere an HttpCall is accessible — from a helper function, a private function within a controller, or from within the ValidationGuard.

// anywhere from where a call is accessible
call.share(Pair("valuation", 25000000))

// you can append additional context data from
// your controller when calling render
call.render("index", mapOf("name" to "John"))

<h1>Hello, {{ name }}!</h1>
Your value is: ${{ valuation }}

You can also call the data that was "shared" with the template using the, also aptly named, shared(key: String) method.

// retrieves "25000000" from the previously shared valuation 

Configuration Values

Alpas already makes all your configs available in your view templates using a combo of config() function and a convention that is based on the name of your config class — lowercase class name with the suffix Config stripped off.

To give an example, let's say you created an AppConfig class that you extended under a Configs folder in your project and now you want to access the appUrl config you defined in the AppConfig class from one of your templates. You can access it like so: {{ config('app.appUrl') }}.

Notice how AppConfig class is simply referred to as app.

Let's see another example. Say you want to access adminEmail config defined in an AdminConfig class from one of your templates, you'd do: {{ config('admin.adminEmail') }}.

Environment Variables

Just like your config values, an env view function is available in your templates to access any environment variables.

Let's say you want to access the APP_URL environment variable defined in .env file from one of your templates. Simply do: {{ env('APP_URL') }}.

Template Helpers

These are some helper functions that come shipped with Alpas. For Pebble template functions, please consult the official documentation.

Route Helpers

  • route(name, params)

Creates a full URL for the route of the given name name. params is a map of parameters expressed as if like a JSON object: {'page': 'routing', 'ver': '2'}

All required path parameters for the route are first extracted and set and then any remaining values from the map are passed as query parameters to the route.

<!-- the url will be something like: -->

<a href="{{ route('', {'page': 'routing', 'ver': '2'}) }}">
  Show Routing Docs

  • hasRoute(name)

Checks if a route of name exists.

  • routeIs(name)

Checks if the name of the current request route matches name.

  • routeIsOneOf(names)

Checks if the name of the current route matches any of the names.

{% if routeIsOneOf(['docs.index', 'docs.toc']) %}
  <h1>Hello Index and TOC!</h1>
{% endif %}

You can negate the check like so:

{% if not routeIsOneOf(['docs.index', 'docs.toc']) %}
  <h1>Hello, page!</h1>
{% endif %}

Validation Helpers

  • old(key, default)

If exists, returns an old input value for the given key. If not, returns the given default value. If multiple values exist for the given key, it returns a list of all values.

<form action="/users" method="post">
  {% csrf %}
  <input type="text" name="username" value="{{ old('username') }}" />
  <input type="email" name="email" value="{{ old('email', '') }}" />
  <button type="submit">Create</button>

  • errors(key, default)

If exists, returns an error or a list of errors for the given key. If not, returns the given default value. key is optional when calling this function. If you don't pass it, it just returns the errors map.

  • firstError(key, default)

If exists, returns the first error for the given key. If not, return the given default value. Unlike the errors() method, the key here is required.

  • hasError(key)

Returns true if an error exists for the given key. Otherwise, returns false

  • whenError(key, value, default)

If an error exists for the given key, it returns the given value. If not, it returns the default value.

Session Helpers

  • flash(key, default)

Retrieve a flash payload for the given key. If it doesn't exist then return the given default value.

<h1>{{ flash('success', 'Good job!') }}</h1>

  • hasFlash(key)

Checks if a flash item for the given key exists.

{% if hasFlash('success') %}
<h1 class="green">{{ flash('success') }}</h1>
{% else %}
<h1 class="red">Oops! There was an error.</h1>
{% endif %}

Mix Helpers

  • mix(name, directory)

Returns a URL of an asset identified by the given name relative to the resources/web directory. The optional parameter directory is the location where the mix-manifest.json file is relative to the resources/web directory. It's very rare that you'd have to set the second parameter.

<!-- the url will be something like /css/app.css -->
<link rel="stylesheet" href="{{ mix('css/app.css') }}">

<!-- the url will be something like /js/analytics/ga.js -->
<script src="{{ mix('js/analytics/ga.js') }}"></script>

Template Tags

Here are some tags that come shipped with Alpas. For Pebble template tags, please consult the official documentation.

  • {% csrf %}

Generates a hidden input field with its value set to a CSRF token for current request. This is required to protect you against cross-site request forgery (CSRF) attacks. If you just need to the raw csrf token rather than generating an input hidden field, use _csrf variable instead.

  • {% auth %} ... {% else %} ... {% endauth %}

If the current user is authenticated, evaluates the block within. Otherwise, evaluates the else block.

{% auth %}
  <h1>Hello, {{ }}! You are so awesome! </h1>
{% else %}
  <h1> Welcome, guest! You're awesome!</h1>
{% endauth %}

  • {% guest %} ... {% else %} ... {% endguest %}

Just opposite of auth tag—if the current user is a guest, evaluates the block within. Otherwise, evaluates the else block.

{% guest %}
  <h1> Welcome, guest! You're awesome!</h1>
{% else %}
  <h1>Hello, {{ }}! You are so awesome! </h1>
{% endguest %}

Template Filters

Filters modify the output further such as converting a text to uppercase, concatenating all collection items to a string, etc. Here are some additional filters provided by Alpas. Pebble comes with lots of very handy filters.

  • int

Attempts to casts the given variable to an integer otherwise returns null. Internally performs ?.toString().toIntOrNull() on the given input.

  • json_encode

Converts the given input to JSON using Jackson. This is a very useful filter when passing data as a JSON encoded string to a frontend library like React, VueJS etc.

<h1> Your Tasks</h1>
<task-list :tasks="{{ tasks is empty ? '[]' : tasks | json_encode }}"></task-list>

  • ago

Displays an instance of LocalDateTime or Instant object as relative time ago language. Internally uses TimeAgo to perform the actual conversion.

<h3>This task was created: {{ task.createdAt | ago }}</h3>

Extending Pebble

Extensibility is at the heart of Pebble. Creating your own tags, functions, filters, tests, and a global variable is very straight forward.

Creating Pebble Extensions

You write a Pebble Extension is Alpas by simply implementing the dev.alpas.view.extensions.PebbleExtension and overriding the appropriate methods such as tests(), filters(), etc. Each of these methods receive an instance of an Application to make it easy for you to resolve any dependencies you need.

class MyPebbleExtension : PebbleExtension {
    override fun tokenParsers(app: Application): List<TokenParser>? {
        TODO("Return a list of custom token parsers")

    override fun filters(app: Application): Map<String, Filter>? {
        TODO("Return a list of custom filters")

/tip/The discovery of your extension is done automatically by Alpas; you don't need to register it or do anything. Just make sure your extension is in the class path.

Let's see a real example of writing a ago custom filter and registering it in two very simple steps:

  1. Create the actual filter.

class AgoFilter : Filter {
    override fun getArgumentNames(): List<String>? {
        return null

    override fun apply( input: Any?,
        args: Map<String, Any>?,
        self: PebbleTemplate,
        context: EvaluationContext?,
        line: Int
    ): Any? {
        return when (input) {
            is Instant -> TimeAgo.using(input.toEpochMilli())
            is LocalDateTime -> TimeAgo.using(input.toInstant(ZoneOffset.UTC).toEpochMilli())
            else -> {
                val name =
                val message = "ago needs an Instant or LocalDateTime. Called from $name#$line."
                throw Exception(message)

  1. Create an extension and return a list of filters, tags, functions etc.

class MyPebbleExtension : PebbleExtension {
    override fun filters(app: Application): Map<String, Filter> {
        return mapOf("ago" to AgoFilter())

Pebble has a whole page dedicated on how to do it. Just keep in mind that for every Pebble's extension function there is an overloaded version that is passed the app instance of resolving bindings.

We highly recommend that you go through the official Pebble documentation on extensibility to learn more about it.

Read the source code on how they have implemented their own filters, tags, functions, etc. before writing your own.

Also, keep in mind that very rarely you'd need to write more than one extension as an extension can return a list of filters, tags, functions, etc.

Adding Custom Tags

Alpas lets you quickly register a custom Pebble tag without having to write a complete token parser. Inside the register() method of your custom extension, you can add a new tag by its name and a callback that receives a TagContext object. All you need to do is return the actual string that should get rendered.

Let's say we want to write a custom tag greet that would just greet a name variable within an <h1> tag. This is how you'd create this custom tag.

class MyPebbleExtension : PebbleExtension() {
    override fun register(app: Application, customTags: CustomTags) {
        customTags.add("header") {
            """<h1 class="font-lg color-red">Hello, {{ name }}!</h1>"""

You can now use this new custom tag of yours in any templates.

<div> {% greet %} </div>

{# should render something like: <div> Hello, Jane! </div> #}

Adding Custom Conditional Tags

A conditional tag is similar to an if/else tag but is customized for a specific purpose that could be reused. Let's say you want to render a block of HTML tags, or include a partial template only if your app is in production mode. You can achieve this by using an if tag by doing something like:

<div class="text-lg">

{% if env.isLocal or env.isDev %}
<p>Render this only in dev mode.</p>
{% else %}
<p>Render this only in prod mode.</p>
{% endif %}


Instead of scattering this if conditional logic, you can write a conditional tag, say dev, and use this tag instead. Here's how you'd do it.

class MyPebbleExtension : PebbleExtension() {
    override fun register(app: Application, conditionalTags: ConditionalTags) {
        conditionalTags.add("dev") {
            // the current HttpCall instance is available here as 'call'
            call.env.isLocal || call.env.isDev

You can now use this conditional dev tag anywhere in your templates.

<div class="text-lg">

{% dev %}
<p>Render this only in dev mode.</p>
{% else %}
<p>Render this is only prod mode.</p>
{% enddev %}


Adding Custom Functions

You can also register your custom function easily with Alpas by just overriding an overloaded register method. Function is very similar to a tag but it outputs some text and accepts zero or multiple arguments.

Here is how the {{ spoof() }} method is actually implemented in Alpas.

class MyPebbleExtension : PebbleExtension() {
    override fun register(app: Application, customFunctions: CustomFunctions) {
        customFunctions.add("spoof", listOf("method")) {

            val method = args?.get("method")
                ?: throw Exception("spoof() function requires the name of the method to spoof")

            """<input type="hidden" name="_method" value="${method.toString().toLowerCase()}">"""

Syntax Highlighting

One great advantage of Pebble being a sub-set of Twig templating engine is that JetBrains has an official Twig Plugin that provides a handful of twig features — syntax highlighting, auto-completion, live templates, etc. To apply this plugin, your templates must end with a .twig extension and IntelliJ IDEA will pick it up automatically.

Unfortunately, this plugin is only available for the Ultimate Edition of InteliJ IDEA and not for the free community version. Fortunately, there is another free extension that is equally good but is totally free and open-source and is made specifically for Pebble. The plugin, aptly named Pebble IntelliJ, is available on GitHub for free.

To apply this plugin, your templates must end with a .peb extension. Make sure to read the official doc on how to set it up properly.

/watch/ Watch Setting up Pebble IntelliJ plugin for your Alpas app by AlpasCasts.

/power/ Alpas's templating is proudly powered by Pebble Templates.