Reference theme: ls_shop/themes/demo/, a working example of every case below.
1. Creating a Theme
Go to Desk → iVend Theme → New.
Set theme_name (e.g. "Demo"). Save.
This auto-scaffolds:
ls_shop/themes/demo/, your template filesls_shop/public/themes/demo/, your static assets (css, js, images)
To activate: open the record, check is_active, save. Only one theme can be active at a time, activating one deactivates the rest.
Your theme folder structure:
themes/demo/
pages/ ← page templates (one file per route)
components/
includes/ ← header, footer overrides
core/ ← language selector, search bar, pagination, etc.
macros/ ← item card, icon, modal overrides
2. Adding Custom Pages
Drop any .html file inside pages/ and it becomes a route automatically. No registration needed.
themes/demo/pages/about.html → /en/about and /ar/about
Example: themes/demo/pages/about.html:
{% extends "templates/layout.html" %}
{% block body %}
<div class="demo-about">
<h1>About Us</h1>
<p>This is a custom page. No registration needed, just drop a .html file in pages/.</p>
<p>Access this page at <strong>/{{ lang }}/about</strong>.</p>
</div>
{% endblock %}
Nested paths work too: pages/lookbook/summer.html → /en/lookbook/summer
3. Overriding an Existing Component
Every component in templates/ can be overridden by placing a file at the same relative path under components/ in your theme.
ThemeFallbackLoader checks your theme first, then falls back to ls_shop defaults. No imports, no registration, just drop the file.
Example: themes/demo/components/core/language_selector.html:
<div class="demo-lang-selector" x-data>
<a href="#" @click.prevent="window.location.href = window.location.pathname.replace(/^\/(en|ar)/, '/en')"
class="demo-lang-btn {% if lang == 'en' %}active{% endif %}">EN</a>
<span>|</span>
<a href="#" @click.prevent="window.location.href = window.location.pathname.replace(/^\/(en|ar)/, '/ar')"
class="demo-lang-btn {% if lang == 'ar' %}active{% endif %}">AR</a>
</div>
Note: Use
window.location.pathname(client-side) for path switching. Do NOT userequest.path, it is not available in Frappe Jinja templates.
Same pattern works for:
components/includes/header.html, site headercomponents/includes/footer.html, site footercomponents/core/search_bar.htmlcomponents/core/pagination.htmlcomponents/core/sort_by.html
4. Static Assets
Put your CSS, JS, and images here:
ls_shop/public/themes/demo/css/ls_shop/public/themes/demo/js/ls_shop/public/themes/demo/images/
They are served at: /assets/ls_shop/themes/demo/css/demo.css
In templates, always use shop_theme_asset_url(), it resolves the active theme slug automatically.
Example: themes/demo/pages/index.html:
{% block head %}
<link rel="stylesheet" href="{{ shop_theme_asset_url('css/demo.css') }}">
{% endblock %}
{% block script %}
<script src="{{ shop_theme_asset_url('js/demo.js') }}"></script>
{% endblock %}
<img src="{{ shop_theme_asset_url('images/banner.jpg') }}">
After adding files to public/, run:
bench build --app ls_shop
5. Adding and Using Macros
Create a macro file in components/macros/.
Example: themes/demo/components/macros/discount_badge.html:
{% macro discount_badge(percent) %}
<span class="demo-badge">{{ percent }}% OFF</span>
{% endmacro %}
Import and call it in any theme page using the templates/macros/... path. ThemeFallbackLoader resolves it to your theme version automatically.
Example: themes/demo/pages/index.html:
{% from "templates/macros/discount_badge.html" import discount_badge %}
{% for p in products %}
{{ p.display_name }}
{% if p.discount_percent %}
{{ discount_badge(p.discount_percent | int) }}
{% endif %}
{% endfor %}
To override an existing ls_shop macro (e.g. item_card):
- Place override at
themes/demo/components/macros/item_card.html - Import via
{% from "templates/macros/item_card.html" import item_card %} - Your version is used everywhere
item_cardis imported