Migrating from iocaine 1.x to 2.0
Last updated: 2025-09-07

> Important

> There are multiple breaking changes in iocaine 2.0. While your configuration will likely continue to remain valid, it might not work the same way as before. If you customized the template, it will need to be ported, as the template engine changed between 1.x and 2.0.

The biggest breaking change in iocaine 2.0 is the template engine: while 1.x used handlebars syntax, 2.0 uses Jinja2. The two are not compatible. Before diving into the differences, please read the templating documentation!

Because the new templating system is more powerful, a handful of configuration options have been removed: everything under `[generator.markov]` and `[generator.links]` are gone, and can safely be removed. It is now the responsibility of the template to generate the appropriate number of paragraphs and links. The default template matches the default configuration values of iocaine 1.x.

- Whereas in iocaine 1.x one would have written the following:

    {{#each paragraphs}}
    <p>{{ markov-gen "garbage" this.index this.value }}</p>
    {{/each}}

In iocaine 2.0, the number of paragraphs, and the words within each are not determined by the configuration anymore, but by the template, with the following syntax:

    {% for p in range(rand(group="paragraphs", max=3)) %}
      <p>{{ markov_gen(min=10, max=128) }}</p>
    {% endfor %}

Similarly, the `links` variable no longer exists, either, and instead of the iocaine 1.x syntax:

    <ul>
      {{#each links}}
        <li>
          <a href="{{ href-gen "links" this.index this.value.href_words }}/">
            {{ markov-gen "titles" this.index this.value.title_words }}
          </a>
        </li>
      {{/each}}
    </ul>

We must now do the looping ourselves:

    <ul>
      {% for p in range(rand(group="links", max=5)) %}
        <li>
          <a href="{{ href_gen(max=2) }}/">
            {{ markov_gen(group="titles", min=4, max=8) }}
          </a>
        </li>
      {% endfor %}
    </ul>

As you see, the name and syntax of the functions changed, too. What used to be `markov-gen` is now `markov_gen`, and `href-gen` is `href_gen`. See the templating documentation for more information about their syntax.

An important question to answer here centers around generating the same thing twice. In iocaine 1.x, one could use `markov-gen "group" <INDEX> <WORDS>`. If `<INDEX>` was the same, then iocaine generated the same output. In iocaine 2.x, the same can be achieved by the `pos` argument: `markov_gen(group="group", pos=<INDEX>, ...)`. For example, if we want to generate the same text for the `<title>` and `<meta name="og:title">` tags, you can do this:

    <title>{{ markov_gen(group="title", max=14) }}</title>
    <meta name="og:title" content="{{ markov_gen(group="title", max=14, pos=0) }}">

As far as functions go, the `is-root` function is also gone, you can simply compare `request_uri` against an empty string:

    {% if request_uri != "" %}
    <a href="../">Back</a>
    {% endif %}

Apart from the template engine change, and the resulting disappearance of some configuration options, another breaking change is that per-host templates are no longer supported by default. Previously, in iocaine 1.x, any template you put into `hosts/<hostname>.hbs` under the template directory (where `<hostname>` is whatever appeared in the `Host` header, verbatim) would have been used instead of the `main.hbs` template. That is no longer the case in iocaine 2.0. Per-host templates are still possible, but it requires setting up the main template appropriately. Place something like the following in `main.jinja`:

    {% set host_template = ("hosts/" ~ request_host|sanitize_filename ~ ".jinja") %}
    {% include [host_template "default.jinja"] ignore missing %}

…and your fallback template into `default.jinja`. Then, host-specific templates can go into `hosts/localhost.jinja`, for example.

That’s about it. It sounds like a lot, but hopefully the migration will not be too hard.