Gesttalt 0.4.0 brings a significant change to how you write templates. We've moved from Mustache to Liquid, powered by liquidz, a Liquid template parser written in Zig.

Why the Change?

Mustache served us well initially. Its "logic-less" philosophy and frozen specification aligned with Gesttalt's goal of stability. But as we built more themes and templates, limitations became apparent.

Conditionals were awkward. Mustache's section syntax works for simple boolean checks, but anything more complex requires workarounds. Showing different content based on which page you're on meant creating boolean flags for every route type.

No filters or transformations. Need to uppercase a title? Format a date? Truncate text? Mustache can't do it. You have to prepare everything in the data layer, mixing presentation concerns with content logic.

Iteration limitations. Mustache loops are basic. There's no first or last check, no loop index, no ability to limit iterations. Common patterns require ugly hacks or duplicate code.

Liquid solves all of these while maintaining template simplicity.

What Liquid Brings

Liquid is Shopify's template language, battle-tested across millions of storefronts. It strikes the right balance between expressiveness and simplicity.

Real conditionals:

{% if route_blog_post %}
  <title>{{ post.title }} - {{ site_title }}</title>
{% elsif route_home %}
  <title>{{ site_title }}</title>
{% endif %}

Filters for transformations:

{{ post.date | date: "%B %d, %Y" }}
{{ description | truncate: 160 }}
{{ title | upcase }}

Powerful loops:

{% for post in posts %}
  {% if forloop.first %}<div class="featured">{% endif %}
  <article>{{ post.title }}</article>
  {% if forloop.last %}</div>{% endif %}
{% endfor %}

Includes with variables:

{% include 'card' with post %}

Why liquidz?

We didn't want to shell out to Ruby or add a C dependency. Gesttalt is written in Zig for a reason: single binary, cross-platform, zero runtime dependencies.

liquidz is a Liquid parser written entirely in Zig. It compiles directly into Gesttalt with no external dependencies. The parser handles the core Liquid syntax that templates need: variables, filters, tags, conditionals, loops, and includes.

This means Gesttalt remains what it's always been: a single binary you can drop anywhere and run. No Ruby, no Node, no runtime. Just your content and themes.

Migration Guide

If you have existing Mustache templates, here are the key changes:

Partials:

{{> header}}        →  {% include 'header' %}

Variables:

{{ title }}         →  {{ title }}  (same!)
{{{ html }}}        →  {{ html }}   (Liquid auto-escapes by default, use raw filter if needed)

Conditionals:

{{#show_nav}}       →  {% if show_nav %}
  ...                    ...
{{/show_nav}}       →  {% endif %}

Loops:

{{#posts}}          →  {% for post in posts %}
  {{title}}              {{ post.title }}
{{/posts}}          →  {% endfor %}

Empty checks:

{{^posts}}          →  {% if posts.size == 0 %}
  No posts             No posts
{{/posts}}          →  {% endif %}

Looking Forward

Liquid opens up possibilities for richer themes. Date formatting, string manipulation, conditional layouts based on content type, and more become straightforward. Theme authors can build more sophisticated designs without fighting the template language.

The core philosophy remains unchanged: stable, predictable, no-surprise static site generation. We've just given template authors better tools to work with.

Upgrade to 0.4.0 and start using Liquid templates today. Your existing Mustache files need updating, but the migration is straightforward. Check the liquidz documentation for the full list of supported tags and filters.