
Jinja in Zid themes: the templates, objects, filters and patterns you'll use every day
How Vitrin themes use Jinja: file layout, inheritance and includes, documented objects and helpers, product loops, translations, brands and the coupon-field setting.
Where Jinja lives in a theme
Zid themes are powered by the Jinja templating engine. It has two forms: {{ ... }} outputs a value, and {% ... %} handles logic, loops and template structure. Zid supports Jinja's core filters and layers a small set of platform tags, filters and macros on top. This is the required folder structure:
├── locale/ar/LC_MESSAGES/messages.po
├── assets/
├── sections/
│ ├── slider.jinja
│ └── slider.schema.json
├── components/
│ └── pagination.jinja
├── templates/
│ ├── home.jinja
│ ├── cart.jinja
│ └── product.jinja
├── layout.jinja
├── layout.schema.json
├── footer.jinja
├── footer.schema.json
├── header.jinja
└── header.schema.json- Template and layout names and directories must be the ones the system recognizes (such as
home.jinja,product.jinja,cart.jinja). Sections and components can be named as you like. - Each section
.jinjaneeds a matching.schema.jsonin the same directory, and the layout, header and footer needlayout.schema.json,header.schema.jsonandfooter.schema.jsonin the theme root. - Assets live in
assets/, are fingerprinted and served through Zid's CDN.
Inheritance, blocks and includes
Every template extends layout.jinja and fills its content block; the Template Library examples use a main_content block. Here's an excerpt of the product.jinja example from the docs:
{% extends "layout.jinja" %}
{% block main_content %}
<h1>{{ product.name }}</h1>
<div class="price">
{% if product.selected_product.formatted_sale_price %}
<span>{{ product.selected_product.formatted_sale_price }}</span>
<del>{{ product.selected_product.formatted_price }}</del>
{% else %}
{{ product.selected_product.formatted_price }}
{% endif %}
</div>
{{ product.description | safe }}
{% include 'vitrin:products/variants_list.jinja' %}
{% include 'vitrin:shared/bank_discounts.jinja' %}
{% endblock %}includeinserts another template at render time, such ascomponents/pagination.jinja. To pass a small local context:{% with a=1 %}{% include 'x' %}{% endwith %}.- Platform-managed templates are included with the
vitrin:prefix, such asvitrin:products/variants_list.jinjaorvitrin:cart/products_list.jinja. - On the home page,
{% template_components %}renders the sections merchants arrange in the Theme Editor.
Objects available in templates
Each page in the Template Library lists its variables. store and session are available on the documented templates, alongside a page-specific object:
productonproduct.jinja:name,slug,description(HTML) andselected_product, the current variant withformatted_price,formatted_sale_price,in_stock,skuandmedia. Alsovariants,options,rating,related_productsandbrand.brandandproductsonbrand.jinja(route/brands/{slug_brand}): the brand hasid,name,slug,descriptionandlogo; the product list hasresults,count,page,pages_countandfilters. The list of all brands isbrands.jinjaat/brands.settings: a flattened version of your schema. Alogosetting in theheadergroup becomessettings.header_logo. Settings inside a list become a list of objects you loop over withsafeget(settings, "images", []).session:session.query_params.get('q')for query parameters,session.url.pathfor the current path, andsession.url.include_query_params(page=2)to build a URL with extra query parameters.store.settings: store-level settings, such asstore.settings.checkout.is_coupon_form_enabled.
results, count, page, page_size and pages_count, replacing old Twig keys such as data, total and last_page.Platform helpers and filters
asset_url(filter): returns a versioned CDN URL for a file inassets/.url_for: builds a route URL from an operation name and respects locale and routing rules, such asproduct_details,list_products,cart_pageorfaqs. It acceptsquery_paramsto append query strings. Brand routes aren't supported yet, so use/brandsand/brands/{{ brand.slug }}.image_url(macro):image_url(path, w=None, h=None, q=None, f=None).wandhare pixels,qis quality from 0 to 100, andf='auto'lets the browser pick the best format (AVIF or WebP).- Date and time:
today(),now(),format_datetimeandformat_time, with timezone and locale already set. - Jinja core filters work as usual, including
upper,length,safe(render raw HTML),default(ord),replace,tojsonandurlencode.
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
<a href="{{ url_for('product_details', slug=product.slug) }}">{{ product.name }}</a>
<a href="{{ url_for('faqs') }}">Help & FAQs</a>
<img src="{{ image_url(my_image, w=400, q=85, f='auto') }}" alt="">
{{ format_datetime(order.created_at, fmt="long") }}Common patterns: loops, conditionals and null safety
The pattern you'll write most is a loop over a product list with filters and pagination. This is an excerpt of the brand.jinja example from the docs, and product listing pages follow the same shape:
{% extends "layout.jinja" %}
{% block main_content %}
<h1>{{ brand.name }}</h1>
{% if products.filters | length > 0 %}
{% include 'vitrin:products/filters.jinja' %}
{% endif %}
<div class="products">
{% for product in products.results %}
{% include 'components/product-card.jinja' %}
{% endfor %}
</div>
{% include 'components/pagination.jinja' %}
{% endblock %}- Jinja handles chained access to null values safely, but iterating over a null value or dividing by zero breaks rendering. Guard loops with
{% if var is defined and var is iterable %}. - For defaults:
{{ title or 'Untitled' }}or{{ title | default('Untitled') }}.default('x', true)also treats falsy values such as''and0as missing. - To show the brand on a product page, check
{% if product.brand %}first, then renderproduct.brand.logoandproduct.brand.name. - For the current variant's availability use
product.selected_product.in_stock, andproduct.selected_product.quantityfor stock.
Translations with gettext
Templates contain literal English source strings, and locale/ar/LC_MESSAGES/messages.po maps each string (msgid) to its translation (msgstr). The core forms:
{{ _("Add to cart") }}
{{ _("Hello %(name)s", name=user.name) }}
{% trans count=n %}{{ count }} item{% pluralize %}{{ count }} items{% endtrans %}
{{ pgettext("button", "Open") }}
{{ pgettext("verb", "Open") }}- Keep strings literal and don't build them by concatenation, so extraction tools can find them.
- Use
%(name)splaceholders instead of.format(). {% trans %}with{% pluralize %}leaves plural forms to the locale's rules, which matters for Arabic and its six forms (msgstr[0]tomsgstr[5]).pgettextseparates identical strings with different meanings. Keep everymsgidunique in the file, and keep HTML tags outside translatable text.
Store settings that change what you render: the coupon field
Some merchant settings must be respected in your templates. Merchants control the coupon field under Settings, then Checkout Options, with Show coupon code field (enabled by default), and the theme reads store.settings.checkout.is_coupon_form_enabled:
{% set coupon_form_enabled = store.settings.checkout.is_coupon_form_enabled != false %}
{% if coupon_form_enabled %}
<section class="cart-coupon" data-coupon-section>
{# Coupon input, apply button, applied coupon details, and remove button #}
</section>
{% endif %}- Use
!= false, not a plain truthy check, so the section stays visible when the value isnullor missing. - Hide the complete section: input, apply button, applied coupon, remove button, and validation and error messages.
- Don't hide it with CSS. The Jinja condition keeps the HTML from being generated at all.
- Add null checks in JavaScript before touching coupon elements, and apply the condition on every cart surface.
- Store settings may be cached for up to 10 minutes, so wait before judging a test result.
Key takeaways
- Every template extends
layout.jinja, and platform-managed templates are included with thevitrin:prefix. - Rely only on documented objects:
store,session,settingsand the page object such asproductorproducts. - Use
asset_url,image_urlandurl_forinstead of hardcoded URLs, except for brand routes. - Wrap text in
_()and{% trans %}, with a uniquemsgidfor each string. - Guard against null values, and hide the coupon section only when the setting is explicitly
false.
Sources
Start building on Zid
Sign up in the partner portal, then build and test on a development store.


