Template Engine

Azu's template engine is built on top of Crinja, a Jinja2-compatible templating engine for Crystal. It provides powerful server-side rendering capabilities with type-safe data binding and automatic escaping.

Overview

The template engine provides:

  • Jinja2-compatible syntax for familiar templating

  • Type-safe data binding with compile-time validation

  • Automatic escaping for security

  • Template inheritance and includes

  • Custom filters and functions for data transformation

Basic Usage

Template Rendering

struct UserProfileResponse
  include Response
  include Templates::Renderable

  def initialize(@user : User)
  end

  def render
    view "user_profile.html", {
      user: @user,
      is_admin: @user.admin?,
      posts: @user.recent_posts
    }
  end
end

Template File (user_profile.html)

{% extends "base.html" %} {% block title %}{{ user.name }}'s Profile{% endblock
%} {% block content %}
<div class="user-profile">
  <h1>{{ user.name }}</h1>
  <p class="email">{{ user.email }}</p>

  {% if is_admin %}
  <div class="admin-badge">Administrator</div>
  {% endif %}

  <div class="user-stats">
    <span>Posts: {{ posts.size }}</span>
    <span>Joined: {{ user.created_at | date("%B %Y") }}</span>
  </div>

  {% if posts.any? %}
  <div class="recent-posts">
    <h2>Recent Posts</h2>
    {% for post in posts %}
    <article class="post">
      <h3>{{ post.title }}</h3>
      <p>{{ post.excerpt }}</p>
      <time>{{ post.created_at | date("%Y-%m-%d") }}</time>
    </article>
    {% endfor %}
  </div>
  {% else %}
  <p>No posts yet.</p>
  {% endif %}
</div>
{% endblock %}

Template Syntax

Variables

Variables are accessed using double curly braces:

<h1>{{ user.name }}</h1>
<p>Email: {{ user.email }}</p>
<p>Age: {{ user.age | default("Not specified") }}</p>

Control Structures

If Statements

{% if user.admin? %}
<div class="admin-panel">
  <h2>Admin Controls</h2>
  <!-- Admin content -->
</div>
{% elif user.moderator? %}
<div class="moderator-panel">
  <h2>Moderator Controls</h2>
  <!-- Moderator content -->
</div>
{% else %}
<p>Regular user account.</p>
{% endif %}

For Loops

<ul class="user-list">
  {% for user in users %}
  <li class="user-item">
    <span class="name">{{ user.name }}</span>
    <span class="email">{{ user.email }}</span>
    {% if user.active? %}
    <span class="status active">Active</span>
    {% else %}
    <span class="status inactive">Inactive</span>
    {% endif %}
  </li>
  {% else %}
  <li class="no-users">No users found.</li>
  {% endfor %}
</ul>

While Loops

{% set counter = 0 %} {% while counter < 5 %}
<div class="item-{{ counter }}">Item {{ counter + 1 }}</div>
{% set counter = counter + 1 %} {% endwhile %}

Template Inheritance

Base Template (base.html)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block title %}Azu Application{% endblock %}</title>
    <link rel="stylesheet" href="/css/app.css" />
  </head>
  <body>
    <header>
      <nav>
        <a href="/">Home</a>
        <a href="/users">Users</a>
        <a href="/posts">Posts</a>
      </nav>
    </header>

    <main>
      {% block content %}
      <!-- Default content -->
      {% endblock %}
    </main>

    <footer>
      <p>&copy; 2024 Azu Application</p>
    </footer>

    <script src="/js/app.js"></script>
  </body>
</html>

Child Template

{% extends "base.html" %} {% block title %}User Profile - {{ user.name }}{%
endblock %} {% block content %}
<div class="user-profile">
  <h1>{{ user.name }}</h1>
  <!-- User profile content -->
</div>
{% endblock %}

Template Includes

Include Template (user_card.html)

<div class="user-card">
  <img src="{{ user.avatar_url }}" alt="{{ user.name }}" class="avatar" />
  <div class="user-info">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <span class="role">{{ user.role }}</span>
  </div>
</div>

Using Include

<div class="user-list">
  {% for user in users %} {% include "user_card.html" %} {% endfor %}
</div>

Filters

Built-in Filters

String Filters

<!-- Uppercase -->
<h1>{{ user.name | upper }}</h1>

<!-- Lowercase -->
<p>{{ user.email | lower }}</p>

<!-- Capitalize -->
<p>{{ user.title | capitalize }}</p>

<!-- Truncate -->
<p>{{ user.bio | truncate(100) }}</p>

<!-- Replace -->
<p>{{ user.description | replace("old", "new") }}</p>

Number Filters

<!-- Format number -->
<p>Posts: {{ user.post_count | number }}</p>

<!-- Round -->
<p>Rating: {{ user.rating | round(2) }}</p>

<!-- Default value -->
<p>Age: {{ user.age | default("Unknown") }}</p>

Date Filters

<!-- Format date -->
<p>Joined: {{ user.created_at | date("%B %d, %Y") }}</p>

<!-- Relative time -->
<p>Last seen: {{ user.last_seen | timeago }}</p>

<!-- Custom format -->
<p>Updated: {{ user.updated_at | date("%Y-%m-%d %H:%M") }}</p>

Array Filters

<!-- Length -->
<p>Posts: {{ posts | length }}</p>

<!-- First/Last -->
<p>First post: {{ posts | first }}</p>
<p>Latest post: {{ posts | last }}</p>

<!-- Sort -->
{% for post in posts | sort(attribute="created_at") %}
<div>{{ post.title }}</div>
{% endfor %}

<!-- Filter -->
{% for post in posts | filter(attribute="published", value=true) %}
<div>{{ post.title }}</div>
{% endfor %}

Custom Filters

Registering Custom Filters

# Register custom filters
Azu::Templates.register_filter("currency") do |value, args|
  amount = value.as(Number)
  currency = args.first? || "USD"

  case currency
  when "USD"
    "$#{amount}"
  when "EUR"
    "€#{amount}"
  else
    "#{amount} #{currency}"
  end
end

Azu::Templates.register_filter("pluralize") do |value, args|
  count = value.as(Number)
  singular = args[0]
  plural = args[1]? || "#{singular}s"

  count == 1 ? singular : plural
end

Using Custom Filters

<p>Price: {{ product.price | currency("USD") }}</p>
<p>{{ post_count | pluralize("post", "posts") }}</p>

Functions

Built-in Functions

<!-- Range -->
{% for i in range(1, 10) %}
  <span>{{ i }}</span>
{% endfor %}

<!-- Random -->
<p>Random number: {{ random(1, 100) }}</p>

<!-- Now -->
<p>Current time: {{ now() | date("%Y-%m-%d %H:%M:%S") }}</p>

<!-- URL -->
<a href="{{ url("user_profile", id=user.id) }}">View Profile</a>

Custom Functions

Registering Custom Functions

# Register custom functions
Azu::Templates.register_function("gravatar_url") do |args|
  email = args[0].as(String)
  size = args[1]? || 80

  hash = Digest::MD5.hexdigest(email.downcase)
  "https://www.gravatar.com/avatar/#{hash}?s=#{size}&d=identicon"
end

Azu::Templates.register_function("format_file_size") do |args|
  bytes = args[0].as(Number)

  case
  when bytes < 1024
    "#{bytes} B"
  when bytes < 1024 * 1024
    "#{(bytes / 1024).round(1)} KB"
  when bytes < 1024 * 1024 * 1024
    "#{(bytes / (1024 * 1024)).round(1)} MB"
  else
    "#{(bytes / (1024 * 1024 * 1024)).round(1)} GB"
  end
end

Using Custom Functions

<img src="{{ gravatar_url(user.email, 120) }}" alt="{{ user.name }}" />
<p>File size: {{ format_file_size(file.size) }}</p>

Configuration

Template Configuration

# Configure template engine
Azu::Configuration.configure do |config|
  # Template directory
  config.template_path = "templates"

  # Enable template caching in production
  config.template_cache = production?

  # Custom template extensions
  config.template_extensions = [".html", ".jinja", ".template"]

  # Auto-reload templates in development
  config.template_auto_reload = development?

  # Template encoding
  config.template_encoding = "UTF-8"
end

Environment-Specific Configuration

# Development environment
Azu::Environment.configure :development do |config|
  config.template_cache = false
  config.template_auto_reload = true
  config.template_debug = true
end

# Production environment
Azu::Environment.configure :production do |config|
  config.template_cache = true
  config.template_auto_reload = false
  config.template_debug = false
end

Security

Automatic Escaping

The template engine automatically escapes output to prevent XSS attacks:

<!-- User input is automatically escaped -->
<p>{{ user_input }}</p>

<!-- Raw output (use with caution) -->
<p>{{ user_input | safe }}</p>

CSRF Protection

<!-- Include CSRF token in forms -->
<form method="POST" action="/users">
  <input type="hidden" name="_csrf" value="{{ csrf_token() }}" />
  <input type="text" name="name" value="{{ user.name }}" />
  <button type="submit">Update</button>
</form>

Performance

Template Caching

# Enable template caching
Azu::Configuration.configure do |config|
  config.template_cache = true
  config.template_cache_size = 1000
end

Fragment Caching

class UserListComponent < Azu::Component
  def content
    div class: "user-list" do
      users.each do |user|
        # Cache individual user fragments
        cached_fragment "user:#{user.id}", ttl: 300 do
          render_user_card(user)
        end
      end
    end
  end
end

Template Precompilation

# Precompile templates in production
if production?
  Azu::Templates.precompile_all!
end

Error Handling

Template Error Handling

struct ErrorResponse
  include Response
  include Templates::Renderable

  def initialize(@error : Exception)
  end

  def render
    begin
      view "error.html", {
        error: @error,
        message: @error.message,
        backtrace: development? ? @error.backtrace : nil
      }
    rescue ex
      # Fallback to simple error response
      {
        error: "Template rendering failed",
        message: ex.message
      }.to_json
    end
  end
end

Template Debugging

# Enable template debugging
Azu::Configuration.configure do |config|
  config.template_debug = development?
  config.template_debug_info = true
end

Best Practices

1. Template Organization

  • Use logical directory structure

  • Keep templates focused and single-purpose

  • Use template inheritance for consistent layouts

  • Organize includes and macros in separate files

2. Performance

  • Cache frequently used templates

  • Use fragment caching for expensive parts

  • Minimize database queries in templates

  • Precompile templates in production

3. Security

  • Never trust user input

  • Use automatic escaping

  • Validate all template data

  • Use CSRF protection for forms

4. Maintainability

  • Use descriptive variable names

  • Keep templates simple and readable

  • Document complex template logic

  • Use consistent naming conventions

Next Steps


Ready to build templates? Start with the basic syntax examples above, then explore the Markup DSL for component-based development.

Last updated

Was this helpful?