Upgrade your tabi site to Zola 0.23

Zola 0.23.0 changed many things, mostly due to the upgrade to Tera 2.

Since this required users making changes, I’ve taken the opportunity to deprecate existing settings that only existed for backwards compatibility. This documents aims to facilitate the transition.


tabi requires Zola 0.23.6 or newer. Check the version used locally, in CI, and for deployment:

zola --version

If you must stay on Zola older than 0.23.0, pin tabi to v4.2.0, the final compatible release.

Existing clone:

git -C themes/tabi fetch --tags
git -C themes/tabi checkout --detach v4.2.0
git -C themes/tabi describe --tags --exact-match

Submodule:

git submodule update --init themes/tabi
git -C themes/tabi fetch --tags
git -C themes/tabi checkout --detach v4.2.0
git add themes/tabi
git diff --cached --submodule=log -- themes/tabi
git commit -m "Pin tabi v4.2.0 for pre-0.23 Zola"

The parent repository records the submodule commit, not the tag name. For a new clone:

git clone --branch v4.2.0 --depth 1 https://github.com/welpo/tabi.git themes/tabi

Table of Contents


Remove retired settings

  • footnote_backlinks: use [markdown].bottom_footnotes = true.
  • add_src_to_code_block: use named code fences and code_block_name_links.
  • [extra].index_format: move it to [search] and each enabled language search table.
  • translate_copyright and translated_copyright: use copyright_translations.

Update date formats

If you set long_date_format, short_date_format, archive_date_format, or date_formats, rewrite each value using UTS-35 patterns. Strftime % directives fail when tabi applies the language locale.

# Before
long_date_format = "%d %B %Y"

# After
long_date_format = "dd MMMM y"

Common replacements are %-dd, %ddd, %bMMM, %BMMMM, and %Yy. Quote literal text: d 'de' MMMM 'de' y.

Find what needs changing

Find inline shortcode calls:

grep -rEn --include='*.md' '[{][{][[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*[(]' content

Find block shortcode calls:

grep -rEn --include='*.md' '[{]%[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*[(]' content

Each hit is a file, line number, and the call. Two kinds show up:

  • Shortcodes like admonition, toc, or dual_theme_image. Convert these to component syntax, as shown below.
  • Built-in functions like get_url, resize_image, or get_taxonomy_url. These still work in 0.23. Leave them alone.

If you wrapped examples in {% raw %}, those hits are literal text. Leave them too.

List custom shortcodes and macros:

ls templates/shortcodes templates/macros

Anything listed is yours, not tabi’s, and needs porting to templates/components/.

Check the config for retired settings:

grep -En 'footnote_backlinks|add_src_to_code_block|translate_copyright|translated_copyright|index_format|highlight_code|highlight_theme' config.toml

Update syntax highlighting

Move highlighting settings from [markdown] to [markdown.highlighting]:

[markdown]
smart_punctuation = true

[markdown.highlighting]
theme = "catppuccin-frappe"
style = "class"
error_on_missing_language = true

For separate themes:

[markdown.highlighting]
light_theme = "catppuccin-latte"
dark_theme = "catppuccin-frappe"
style = "class"
error_on_missing_language = true

tabi requires style = "class". Zola generates giallo.css, or giallo-light.css and giallo-dark.css.

The root [search] table configures the default language. Every other language with build_search_index = true needs its own table:

build_search_index = true

[search]
include_title = true
include_description = true
include_path = true
include_content = true
index_format = "elasticlunr_json"

[languages.es]
title = "Mi sitio"
build_search_index = true

[languages.es.search]
include_title = true
include_description = true
include_path = true
include_content = true
index_format = "elasticlunr_json"

Convert shortcodes to components

Inline calls lose parentheses and commas:

-{{ admonition(type="tip", text="Stay hydrated") }}
+{{< admonition type="tip" text="Stay hydrated" />}}

Block calls use named closing tags:

-{% admonition(type="tip") %}
+{% <admonition type="tip"> %}
 Stay hydrated.
-{% end %}
+{% </admonition> %}

Wrap expressions, booleans, numbers, arrays, and maps in braces. Component parameters are explicit by default. Prefix ambient context parameters with @ in the component definition so Tera resolves them from the caller:

{% component post_language(@lang: string) %}
{{ lang }}
{% endcomponent post_language %}
{{< dual_theme_image light_src="light.webp" dark_src="dark.webp" full_width={true} />}}
{{< multilingual_quote original="Hola" translated="Hello" />}}
{{< iine />}}

Move custom shortcodes

Move site-local templates from templates/shortcodes/ to templates/components/ and define a component. This example is adapted from a tabi consumer:

{% component youtube(id: string, class = "", playlist = "", autoplay = false) %}
<div{% if class %} class="{{ class }}"{% endif %}>
    <iframe src="https://www.youtube-nocookie.com/embed/{{ id }}{% if playlist %}?list={{ playlist }}{% if autoplay %}&amp;autoplay=1{% endif %}{% elif autoplay %}?autoplay=1{% endif %}" allowfullscreen></iframe>
</div>
{% endcomponent youtube %}
{{< youtube id="dQw4w9WgXcQ" class="video" autoplay={true} />}}

Use ordinary parameters for the component’s inputs and implicit parameters for ambient values such as lang, config, or page. An implicit value can still be overridden explicitly at the call site.

Protect literal examples

Markdown is templated before fenced code is rendered. Wrap literal examples in {% raw %} and {% endraw %} in the source file.

Custom heading IDs do not need raw blocks:

## Deployment checks {#deployment-checks}

Use skip_content_templating only when the complete file should bypass templating.

Review custom templates

Site overrides under templates/ are not updated with the theme.

OldCurrent
macros and {% import %}components
items | concat(with=item)[...items, item]
items | slice(start=1)items[1:]
value | as_strvalue | str
trim_start_matches / trim_end_matchestrim_start / trim_end
item.0item[0]
is starting_with("http")is starting_with(pat="http")
is iterable for arraysis array
get_taxonomy_url(..., name=term)get_taxonomy_url(..., term=term)
{% include "x" ignore missing %}include an existing default file

Undefined access is stricter. Use optional chaining such as page?.extra?.setting, and guard explicit null values before applying filters. default(value=...) handles undefined values, not null values.

get_page and get_section take a canonical path plus lang. Component arguments use string literals or braced expressions; page is shorthand for page={page}.

See the Tera migration guide for the complete language changes.

Expected output differences

  • Footnotes: bracketed labels and absolute links with #fn-* and #fr-* fragments.
  • Highlighting: numbered z-*, or z-l-*/z-d-*, token classes.
  • Whitespace: differences around component output; check inline content and nesting.
  • Language order: switcher and hreflang order can change; destinations should not.
  • XML: escaping can differ while decoded values remain equivalent.