Liquid Template API Reference
Complete developer reference for all variables, objects, and data available inside Liquid templates. Every section has a config.json and a name.liquid example you can copy into the code editor.
Overview
Every storefront page is assembled from components stored in the database. Each component has two parts:
config.json— declares component metadata, version, and the fields it needs (data sources, editor inputs)name.liquid— the HTML + Liquid template that renders the final output
Global variables
shop and component are injected into every template automatically. You never need to declare them in config.json.How It Works
- The rendering engine reads
config.jsonand inspects thefieldsobject - API fields (products, categories, brands, etc.) are fetched automatically from the backend based on their
type/apiType - Static fields (text, color, number, boolean, image, etc.) are pulled from the visual editor's saved data
- Conditional fields with a
conditionproperty are only fetched when the condition is met - All data is merged into one context:
{ shop, component, ...blockVars, ...staticFields, ...apiData } - Icon paths starting with
/assets/icons/are auto-converted to full backend URLs - The context is passed to LiquidJS which renders the
.liquidtemplate into HTML
File Structure
Components live at a path like category/component-name (e.g. hero/hero-1, products/product-grid-1). The path is auto-generated from the category and name when you create a component.
{
"name": "My Component",
"version": "1.0.0",
"description": "A reusable storefront component",
"fields": {
"title": {
"type": "text",
"label": "Section Title",
"default": "Welcome"
},
"products": {
"type": "products",
"label": "Products"
}
}
}<section class="py-12 px-4">
<h2 class="text-2xl font-bold text-center mb-8">{{ title }}</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-6xl mx-auto">
{% for product in products %}
<a href="/products/{{ product.slug }}" class="border rounded-lg overflow-hidden hover:shadow-md transition">
{% if product.coverImage %}
<img src="{{ product.coverImage }}" alt="{{ product.name }}" class="w-full aspect-square object-cover">
{% endif %}
<div class="p-3">
<h3 class="text-sm font-medium line-clamp-2">{{ product.name }}</h3>
<span class="text-sm font-bold mt-1 block">{{ shop.priceUnit }} {{ product.price }}</span>
</div>
</a>
{% endfor %}
</div>
</section>Data Flow Diagram
Database (ShopWebsiteTemplate)
├─ pages[] → Which components to render on which page
├─ components[] → .liquid content + config.json for each component
└─ blocks[] → Reusable blocks (e.g. product-card) with customization data
│
▼
renderPageComponents()
1. Parse page JSON → list of { path, data }
2. For each component:
a. Read config.json → detect API fields
b. Fetch API data (products, categories, etc.) from tRPC
c. Transform data (array_object wrapping, "automatic" → null)
d. Merge: { shop, component, ...blockVars, ...editorData, ...apiData }
e. Convert icon paths to full URLs
f. LiquidJS.parseAndRender(template, mergedData)
3. Join all rendered HTML
│
▼
Final HTML → sent to browserDefault Component Template
When you create a new component in the developer panel, it starts with this default:
{
"name": "My Component",
"version": "1.0.0",
"description": "Your custom component",
"fields": {
"text": {
"type": "string",
"label": "Your Text",
"value": "Welcome to our store",
"placeholder": "Enter the text"
}
}
}<h2>Your code goes here {{ text }}</h2>shop Object (Global)
The shop object is available in every component and partial. No config needed. It contains all the store's information.
| Property | Type | Description |
|---|---|---|
| shop._id | string | MongoDB shop ID |
| shop.slug | string | Shop subdomain slug (e.g. "my-store") |
| shop.shopName | string | Store display name |
| shop.shopLogo | string | null | Logo image URL |
| shop.shopDescription | string | Store description (max 1000 chars) |
| shop.location | string | Store location / primary address |
| shop.publicEmail | string | Public contact email |
| shop.publicContactNumber | string | Public phone number |
| shop.priceUnit | string | Currency symbol: "Rs", "$", "USD", etc. |
| shop.brandColor | string | Primary brand color (hex) |
| shop.textOverBrandColor | string | Text color to use over brand color |
| shop.brandFont | string | Brand font family name |
| shop.socialMediaLinks | array | Social media links (see below) |
| shop.footerLogo | string | null | Footer logo URL (can differ from main) |
| shop.footerQuickLinks | array | Footer navigation links (see below) |
| shop.panVatNumber | string | null | PAN/VAT registration number |
| shop.ecommerceRegNumber | string | null | Ecommerce platform registration ID |
| shop.privacyPolicy | string | null | Privacy policy (HTML) |
| shop.termsAndConditions | string | null | Terms & conditions (HTML) |
| shop.returnPolicy | string | null | Return policy (HTML) |
| shop.refundPolicy | string | null | Refund policy (HTML) |
| shop.about | string | null | About page (HTML) |
| shop.branches | array | Store branches (see below) |
| shop.paymentMethods | array | Payment method name strings |
| shop.paymentMethodConfig | object | Payment method config (internal) |
shop.branches[]
| Property | Type | Description |
|---|---|---|
| name | string | Branch name |
| location | string | Branch address |
| googleMapLink | string | Google Maps embed or link URL |
| isMainBranch | boolean | Whether this is the main/HQ branch |
{% for branch in shop.branches %}
<div class="p-4 border rounded-lg">
<div class="flex items-center gap-2">
<strong>{{ branch.name }}</strong>
{% if branch.isMainBranch %}<span class="text-[10px] bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">HQ</span>{% endif %}
</div>
<p class="text-sm text-gray-500 mt-1">{{ branch.location }}</p>
{% if branch.googleMapLink %}
<a href="{{ branch.googleMapLink }}" target="_blank" class="text-sm text-blue-500 mt-2 inline-block">Open in Maps →</a>
{% endif %}
</div>
{% endfor %}Policies & About
Policy fields contain HTML content. On privacy, refund, return, and terms pages, policyDesc automatically contains the policy for the current page.
{% if policyDesc %}
<div class="prose max-w-3xl mx-auto py-12">
{{ policyDesc }}
</div>
{% else %}
<p class="text-center text-gray-400 py-16">No policy has been set.</p>
{% endif %}
{%- comment -%}
policyDesc matches the current policy route.
Individual shop fields remain available:
Available policy fields:
- shop.privacyPolicy
- shop.termsAndConditions
- shop.returnPolicy
- shop.refundPolicy
- shop.about
{%- endcomment -%}Payment Methods
shop.paymentMethods is an array of strings like ["COD", "eSewa", "Khalti"].
<div class="flex flex-wrap gap-2">
{% for method in shop.paymentMethods %}
<span class="px-3 py-1 bg-gray-100 text-xs rounded-full">{{ method }}</span>
{% endfor %}
</div>Full Example: Header Component
{
"name": "Simple Header",
"version": "1.0.0",
"description": "Basic header with logo, search, and brand colors",
"fields": {
"showSearch": { "type": "boolean", "label": "Show Search Bar", "default": true },
"announcementText": { "type": "text", "label": "Announcement Bar Text", "default": "" }
}
}{% if announcementText != blank %}
<div class="text-center py-1.5 text-xs font-medium"
style="background-color: {{ shop.brandColor }}; color: {{ shop.textOverBrandColor }};">
{{ announcementText }}
</div>
{% endif %}
<header class="border-b">
<div class="max-w-7xl mx-auto flex items-center justify-between px-4 py-3">
{% if shop.shopLogo %}
<a href="/"><img src="{{ shop.shopLogo }}" alt="{{ shop.shopName }}" class="h-10 object-contain"></a>
{% else %}
<a href="/" class="text-xl font-bold" style="font-family: {{ shop.brandFont }};">{{ shop.shopName }}</a>
{% endif %}
{% if showSearch %}
<div class="hidden md:block flex-1 max-w-md mx-8">
<input type="text" placeholder="Search products..."
class="w-full px-4 py-2 rounded-full bg-gray-100 text-sm outline-none">
</div>
{% endif %}
<div class="flex items-center gap-3 text-sm">
<a href="mailto:{{ shop.publicEmail }}" class="hidden md:block text-gray-600 hover:text-gray-900">Contact</a>
<button onclick="window.openCart && window.openCart()"
class="px-4 py-2 rounded-full text-sm font-medium"
style="background: {{ shop.brandColor }}; color: {{ shop.textOverBrandColor }};">
Cart
</button>
</div>
</div>
</header>component Object (Global)
Metadata about the current component being rendered. Available in every template.
| Property | Type | Description |
|---|---|---|
| component.path | string | Component path, e.g. "hero/hero-1", "products/product-grid-1" |
{%- comment -%} Useful for debugging: show which component is rendering {%- endcomment -%}
<!-- Component: {{ component.path }} -->Block Customization Variables (Global)
These top-level variables come from the active product card block's data field. They control product card rendering and are available in every component context (no config needed). They're merged last, so they override any conflicting keys.
| Property | Type | Description |
|---|---|---|
| borderRadius | string | Card border radius CSS value (e.g. "8px", "0.5rem") |
| imageRounding | string | Product image border radius |
| hoverEffect | string | Hover animation type (e.g. "scale", "shadow", "none") |
| showAddToCart | boolean | Whether to show the Add to Cart button on cards |
| cardPadding | string | Card inner padding CSS value |
| titleSize | string | Product title font size CSS value |
| buttonStyle | string | Button styling variant/class name |
How blocks work
Blocks are reusable sub-components (like a product card design). The active block's customization data is automatically injected into every component's context. When you use{% render 'product-card', product: product %}, the block variables are available in that partial too because LiquidJS shares parent scope.<div class="product-card group" style="border-radius: {{ borderRadius }}; padding: {{ cardPadding }};">
<div class="overflow-hidden" style="border-radius: {{ imageRounding }};">
<img src="{{ product.coverImage }}" alt="{{ product.name }}"
class="w-full aspect-square object-cover {% if hoverEffect == 'scale' %}group-hover:scale-105{% endif %} transition-transform">
</div>
<h3 style="font-size: {{ titleSize }};" class="font-medium mt-2 line-clamp-2">{{ product.name }}</h3>
<div class="flex items-center gap-2 mt-1">
{% if product.compareAtPrice and product.compareAtPrice > product.price %}
<span class="text-xs line-through text-gray-400">{{ shop.priceUnit }} {{ product.compareAtPrice }}</span>
{% endif %}
<span class="font-bold text-sm">{{ shop.priceUnit }} {{ product.price }}</span>
</div>
{% if showAddToCart %}
<button class="{{ buttonStyle }} mt-3 w-full py-2 rounded text-sm font-medium"
style="background: {{ shop.brandColor }}; color: {{ shop.textOverBrandColor }};">
Add to Cart
</button>
{% endif %}
</div>products (Array)
Returns an array of published products from the shop (up to 100). Config type: "products".
{
"name": "Product Grid",
"version": "1.0.0",
"description": "Responsive grid of all shop products",
"fields": {
"title": { "type": "text", "label": "Section Title", "default": "Our Products" },
"columns": { "type": "number", "label": "Columns (desktop)", "default": 4 },
"products": { "type": "products", "label": "Products" }
}
}Product Object Fields
| Property | Type | Description |
|---|---|---|
| name | string | Product name (2-255 chars) |
| slug | string | URL-friendly identifier for routing |
| description | string | Product description (may contain HTML, max 50000 chars) |
| price | number | Current selling price (min 0) |
| compareAtPrice | number | null | Original/strikethrough price for showing discounts |
| costPerItem | number | null | Cost per item (internal, may be exposed) |
| coverImage | string | null | Main product image URL |
| images | string[] | Additional image URLs array |
| quantity | number | Available stock quantity |
| unlimitedStock | boolean | If true, stock is infinite (ignore quantity) |
| trackQuantity | boolean | Whether stock tracking is enabled |
| allowBackorder | boolean | Allow orders when out of stock |
| sku | string | null | Stock keeping unit code |
| barcode | string | null | Product barcode |
| categoryId | string | Category ID reference |
| subcategoryId | string | null | Subcategory ID reference |
| brandId | string | null | Brand ID reference |
| avgReviews | number | Average star rating (0–5, cached) |
| reviewsCount | number | Total number of reviews (cached) |
| productSold | number | Total units sold (cached) |
| tags | string[] | Array of tag strings |
| features | array | Array of feature objects (title, description, image) |
| featuresSectionTitle | string | null | Custom heading for features section |
| specifications | array | Specification groups (see below) |
| extraFields | array | Custom field definitions (see below) |
| isPublished | boolean | Publication status |
| freeDelivery | boolean | Whether delivery is free |
| deliveryTime | string | null | Estimated delivery time text |
| weight | number | null | Product weight value |
| weightUnit | string | "kg", "g", "lb", "oz" |
| isComboProduct | boolean | Whether this is a combo/bundle product |
| metaTitle | string | null | SEO meta title |
| metaDescription | string | null | SEO meta description |
| createdAt | string | Creation date (ISO string) |
| updatedAt | string | Last update date (ISO string) |
<section class="py-12 px-4">
<h2 class="text-2xl font-bold text-center mb-8">{{ title }}</h2>
<div class="grid grid-cols-2 md:grid-cols-{{ columns }} gap-4 max-w-7xl mx-auto">
{% for product in products %}
<a href="/products/{{ product.slug }}" class="group border rounded-lg overflow-hidden hover:shadow-md transition">
{% if product.coverImage %}
<div class="aspect-square bg-gray-50 overflow-hidden">
<img src="{{ product.coverImage }}" alt="{{ product.name }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform" loading="lazy">
</div>
{% endif %}
<div class="p-3">
<h3 class="text-sm font-medium line-clamp-2">{{ product.name }}</h3>
<div class="flex items-center gap-2 mt-1.5">
{% if product.compareAtPrice and product.compareAtPrice > product.price %}
<span class="text-xs line-through text-gray-400">{{ shop.priceUnit }} {{ product.compareAtPrice }}</span>
{% assign saved = product.compareAtPrice | minus: product.price | times: 100 | divided_by: product.compareAtPrice %}
<span class="text-[10px] bg-red-100 text-red-600 px-1.5 py-0.5 rounded">-{{ saved }}%</span>
{% endif %}
<span class="font-bold text-sm">{{ shop.priceUnit }} {{ product.price }}</span>
</div>
{% if product.avgReviews > 0 %}
<div class="flex items-center gap-1 mt-1 text-xs text-yellow-500">
{% for i in (1..5) %}{% if i <= product.avgReviews %}★{% else %}☆{% endif %}{% endfor %}
<span class="text-gray-400">({{ product.reviewsCount }})</span>
</div>
{% endif %}
{% if product.freeDelivery %}<span class="text-[10px] text-green-600 mt-1 block">Free Delivery</span>{% endif %}
{% if product.quantity == 0 and product.unlimitedStock == false %}
<span class="text-[10px] text-red-500 mt-1 block">Out of Stock</span>
{% endif %}
</div>
</a>
{% endfor %}
</div>
</section>Product Variants
Variants are available on single product objects (see product type). Each variant has flexible options.
| Property | Type | Description |
|---|---|---|
| options | object | Map of option names to value arrays: { "Color": ["Red", "Blue"], "Size": ["S", "M", "L"] } |
| price | number | null | Variant-specific price (overrides product price if set) |
| compareAtPrice | number | null | Variant compare/original price |
| sku | string | null | Variant-specific SKU |
| barcode | string | null | Variant barcode |
| quantity | number | Variant stock count |
| coverImage | string | null | Variant-specific image (e.g. different color) |
| weight | number | null | Variant weight |
| weightUnit | string | "kg", "g", "lb", "oz" |
| isActive | boolean | Whether variant is active/visible |
Product Specifications
Specifications are organized in groups. Each group has a title, optional image, and an array of key-value data rows.
{% if product.specifications.size > 0 %}
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">{{ product.featuresSectionTitle | default: "Specifications" }}</h2>
{% for spec in product.specifications %}
<div class="mb-6">
<h3 class="font-semibold text-sm mb-2 flex items-center gap-2">
{% if spec.image %}<img src="{{ spec.image }}" class="w-5 h-5">{% endif %}
{{ spec.specification }}
</h3>
<table class="w-full text-sm border-collapse">
{% for row in spec.data %}
<tr class="border-b">
<td class="py-2 pr-4 font-medium text-gray-600 w-1/3">{{ row.title }}</td>
<td class="py-2 text-gray-800">{{ row.value }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
</div>
{% endif %}Product Extra Fields
Custom fields defined by the shop owner (e.g. "Engraving text", "Gift message"). These are field definitions, not values.
| Property | Type | Description |
|---|---|---|
| label | string | Field label shown to customer |
| fieldType | string | "text", "textarea", or "imageUpload" |
| isRequired | boolean | Whether the customer must fill this |
| placeholder | string | null | Input placeholder text |
{% if product.extraFields.size > 0 %}
<div class="space-y-3 mt-4">
{% for field in product.extraFields %}
<div>
<label class="text-sm font-medium">
{{ field.label }}{% if field.isRequired %}<span class="text-red-500"> *</span>{% endif %}
</label>
{% if field.fieldType == "textarea" %}
<textarea placeholder="{{ field.placeholder }}" class="w-full mt-1 border rounded p-2 text-sm"></textarea>
{% elsif field.fieldType == "imageUpload" %}
<input type="file" accept="image/*" class="mt-1 text-sm">
{% else %}
<input type="text" placeholder="{{ field.placeholder }}" class="w-full mt-1 border rounded px-3 py-2 text-sm">
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}Product Add-ons
product.addOnProducts is available on a single product detail object. It contains up to 20 products selected by the merchant, in the merchant's chosen order. Only published, active products from the same shop are returned.
| Property | Type | Description |
|---|---|---|
| _id | string | Add-on product ID |
| name | string | Add-on product name |
| slug | string | Slug used by the product details URL |
| coverImage | string | null | Main product image URL |
| price | number | Current selling price |
| compareAtPrice | number | null | Optional crossed-out original price |
| quantity | number | Product quantity summary; variants are not included in this compact object |
| unlimitedStock | boolean | Whether stock is unlimited |
| isPublished | boolean | Publication status; returned add-ons are published |
| isActive | boolean | Active status; returned add-ons are active |
| isDeleted | boolean | Deletion status; always false for returned add-ons |
| regionPricing | object | null | Converted currency marker when Multi Region pricing applies |
Add-ons are separate products
Add-ons are recommendations, not combo or bundle lines. They are not automatically included with the main product and must not be silently added to its cart line. Link each one to/products/{{ addOn.slug }} so customers can choose that product's own variants, quantity, and required extra fields. Keep the exact data-zalient-product-add-ons attribute on your outer section; it tells the storefront not to append the default add-ons fallback a second time.{% if product.addOnProducts and product.addOnProducts.size > 0 %}
<section data-zalient-product-add-ons class="mt-10">
<h2 class="text-xl font-bold mb-4">Add-ons</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{% for addOn in product.addOnProducts %}
<a href="/products/{{ addOn.slug }}" class="block rounded-lg border overflow-hidden hover:shadow-md transition">
{% if addOn.coverImage %}
<img src="{{ addOn.coverImage }}" alt="{{ addOn.name }}" class="w-full aspect-square object-cover" loading="lazy">
{% endif %}
<div class="p-3">
<h3 class="text-sm font-medium line-clamp-2">{{ addOn.name }}</h3>
<div class="flex items-baseline gap-2 mt-1">
<span class="font-semibold">{{ shop.priceUnit }} {{ addOn.price }}</span>
{% if addOn.compareAtPrice and addOn.compareAtPrice > addOn.price %}
<span class="text-xs text-gray-400 line-through">{{ shop.priceUnit }} {{ addOn.compareAtPrice }}</span>
{% endif %}
</div>
</div>
</a>
{% endfor %}
</div>
</section>
{% endif %}Product Features
Features are reusable shop-wide highlights (e.g. "Free Returns", "1 Year Warranty") assigned to products.
| Property | Type | Description |
|---|---|---|
| title | string | Feature name |
| description | string | null | Feature description |
| image | string | null | Feature icon/image URL |
{% if product.features.size > 0 %}
<div class="flex flex-wrap gap-4 mt-4">
{% for feature in product.features %}
<div class="flex items-center gap-2 text-sm text-gray-600">
{% if feature.image %}<img src="{{ feature.image }}" class="w-5 h-5">{% endif %}
<span>{{ feature.title }}</span>
</div>
{% endfor %}
</div>
{% endif %}product (Single)
Returns a single product with populated relations. Config type: "product". Supports automatic mode (current page context on /product/:slug) or a hardcoded slug.
{
"name": "Product Detail",
"version": "1.0.0",
"description": "Full product detail page component",
"fields": {
"product": { "type": "product", "label": "Product" }
}
}Automatic vs. Manual
Set to "automatic" in the visual editor to use the current page's product (on /product/:slug pages). Set to a specific slug like "blue-shirt" to always show that product on any page.
The single product includes all fields from the products array above, plus these populated relations:
| Property | Type | Description |
|---|---|---|
| category | object | null | Populated category: { name, slug, image, description } |
| subcategory | object | null | Populated subcategory: { name, slug, image } |
| brand | object | null | Populated brand: { name, slug, image } |
| similarProducts | array | Array of related products (same category) |
| addOnProducts | array | Merchant-selected add-on product summaries; separate from similarProducts and combo items |
{% if product %}
<div class="max-w-6xl mx-auto grid md:grid-cols-2 gap-8 py-8 px-4">
<!-- Images -->
<div>
<img src="{{ product.coverImage }}" alt="{{ product.name }}" class="w-full rounded-lg">
{% if product.images.size > 0 %}
<div class="flex gap-2 mt-4 overflow-x-auto">
{% for img in product.images %}
<img src="{{ img }}" class="w-20 h-20 rounded border object-cover cursor-pointer hover:border-blue-500" loading="lazy">
{% endfor %}
</div>
{% endif %}
</div>
<!-- Info -->
<div>
{% if product.brand %}<a href="/brand/{{ product.brand.slug }}" class="text-sm text-blue-500">{{ product.brand.name }}</a>{% endif %}
<h1 class="text-2xl font-bold mt-1">{{ product.name }}</h1>
{% if product.avgReviews > 0 %}
<div class="flex items-center gap-2 mt-2 text-sm">
<span class="text-yellow-500">{% for i in (1..5) %}{% if i <= product.avgReviews %}★{% else %}☆{% endif %}{% endfor %}</span>
<span class="text-gray-400">({{ product.reviewsCount }} reviews)</span>
<span class="text-gray-400">· {{ product.productSold }} sold</span>
</div>
{% endif %}
<div class="flex items-baseline gap-3 mt-4">
{% if product.compareAtPrice and product.compareAtPrice > product.price %}
<span class="text-lg line-through text-gray-400">{{ shop.priceUnit }} {{ product.compareAtPrice }}</span>
{% endif %}
<span class="text-3xl font-bold" style="color: {{ shop.brandColor }};">{{ shop.priceUnit }} {{ product.price }}</span>
</div>
{% if product.freeDelivery %}<p class="text-sm text-green-600 mt-2">✓ Free Delivery</p>{% endif %}
{% if product.deliveryTime %}<p class="text-sm text-gray-500">Delivery: {{ product.deliveryTime }}</p>{% endif %}
{% if product.quantity == 0 and product.unlimitedStock == false %}
<p class="text-red-500 font-medium mt-4">Out of Stock</p>
{% endif %}
<div class="mt-6 prose prose-sm max-w-none">{{ product.description }}</div>
{% if product.category %}
<p class="text-sm text-gray-500 mt-4">Category: <a href="/category/{{ product.category.slug }}" class="text-blue-500">{{ product.category.name }}</a></p>
{% endif %}
{% if product.tags.size > 0 %}
<div class="flex flex-wrap gap-2 mt-3">
{% for tag in product.tags %}<span class="text-xs bg-gray-100 px-2 py-1 rounded">{{ tag }}</span>{% endfor %}
</div>
{% endif %}
</div>
</div>
{% if product.similarProducts.size > 0 %}
<div class="max-w-6xl mx-auto px-4 pb-12">
<h2 class="text-xl font-bold mb-4">You may also like</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{% for item in product.similarProducts %}{% render 'product-card', product: item %}{% endfor %}
</div>
</div>
{% endif %}
{% endif %}categories
Returns an array of active categories. Config: "type": "api", "apiType": "categories".
{
"name": "Category Grid",
"version": "1.0.0",
"fields": {
"categories": { "type": "api", "apiType": "categories", "label": "Categories" }
}
}| Property | Type | Description |
|---|---|---|
| name | string | Category name |
| slug | string | URL-friendly identifier |
| description | string | null | Category description |
| image | string | null | Category image URL |
| isActive | boolean | Active status |
| order | number | Display order (lower = first) |
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 max-w-6xl mx-auto py-8 px-4">
{% for category in categories %}
<a href="/category/{{ category.slug }}" class="group block rounded-xl overflow-hidden border hover:shadow-lg transition">
{% if category.image %}
<div class="aspect-[4/3] overflow-hidden bg-gray-100">
<img src="{{ category.image }}" alt="{{ category.name }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform" loading="lazy">
</div>
{% else %}
<div class="aspect-[4/3] bg-gray-100 flex items-center justify-center">
<span class="text-3xl font-bold text-gray-300">{{ category.name | slice: 0, 1 | upcase }}</span>
</div>
{% endif %}
<div class="p-4">
<h3 class="font-semibold">{{ category.name }}</h3>
{% if category.description %}<p class="text-sm text-gray-500 mt-1 line-clamp-2">{{ category.description }}</p>{% endif %}
</div>
</a>
{% endfor %}
</div>categories-expanded
Returns categories with their subcategories and products nested inside. Config type: "categories-expanded". This is the data type used for mega menus and navigation.
{
"name": "Mega Menu",
"version": "1.0.0",
"fields": {
"categoryMenu": { "type": "categories-expanded", "label": "Category Navigation" }
}
}| Property | Type | Description |
|---|---|---|
| name | string | Category name |
| slug | string | URL-friendly identifier |
| subcategories | array | Subcategories with products nested (see below) |
| products | array | Products directly under this category (no subcategory) |
Subcategory Object
| Property | Type | Description |
|---|---|---|
| name | string | Subcategory name |
| slug | string | URL slug (or category slug for virtual "All") |
| products | array | Up to 5 products: { name, slug, parentProductName, coverImage } |
Category Direct Products
Virtual "All" subcategory
If a category has products but its subcategories have no products, a virtual subcategory named "All" is automatically prepended to the subcategories array with those direct products. This ensures your mega menu template always has data to render.Each product in the expanded data has:
| Property | Type | Description |
|---|---|---|
| name | string | Product name (or parent product name if variant) |
| slug | string | Product slug for URL |
| parentProductName | string | null | Parent product name (for child/variant products) |
| coverImage | string | null | Product cover image URL |
{% for cat in categoryMenu %}
<div class="category-section">
<a href="/categories/{{ cat.slug }}" class="text-sm font-bold">{{ cat.name }}</a>
{% if cat.subcategories.size > 0 %}
{% for sub in cat.subcategories %}
{% if sub.products.size > 0 %}
<div class="mt-3">
<a href="/categories/{{ cat.slug }}/{{ sub.slug }}" class="text-xs font-semibold text-gray-700">
{{ sub.name }}
</a>
<div class="grid grid-cols-5 gap-2 mt-2">
{% for product in sub.products %}
<a href="/products/{{ product.slug }}" class="block">
{% if product.coverImage %}
<img src="{{ product.coverImage }}" alt="{{ product.name }}"
class="w-full aspect-square object-cover rounded" loading="lazy">
{% endif %}
<p class="text-[11px] mt-1 line-clamp-2">{{ product.name }}</p>
</a>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
{% elsif cat.products.size > 0 %}
{%- comment -%} Fallback: direct category products {%- endcomment -%}
<div class="grid grid-cols-5 gap-2 mt-2">
{% for product in cat.products %}
<a href="/products/{{ product.slug }}" class="block">
{% if product.coverImage %}
<img src="{{ product.coverImage }}" alt="{{ product.name }}"
class="w-full aspect-square object-cover rounded" loading="lazy">
{% endif %}
<p class="text-[11px] mt-1 line-clamp-2">{{ product.name }}</p>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-xs text-gray-400 mt-2">No products found</p>
{% endif %}
</div>
{% endfor %}brands
Returns { brands: [...] }. Note the nested structure: iterate brands.brands. Config: "type": "api", "apiType": "brands".
{ "name": "Brand Logos", "version": "1.0.0", "fields": {
"brands": { "type": "api", "apiType": "brands", "label": "Brands" }
}}| Property | Type | Description |
|---|---|---|
| name | string | Brand name |
| slug | string | null | URL slug |
| image | string | null | Brand logo URL |
| isActive | boolean | Active status |
| order | number | Display order |
<div class="flex flex-wrap gap-6 items-center justify-center py-8">
{% for brand in brands.brands %}
<a href="/brand/{{ brand.slug }}" class="p-4 border rounded-lg hover:shadow-md transition">
{% if brand.image %}<img src="{{ brand.image }}" alt="{{ brand.name }}" class="h-12 object-contain">
{% else %}<span class="text-sm font-medium">{{ brand.name }}</span>{% endif %}
</a>
{% endfor %}
</div>promotions
Array of active promotional offers. Config type: "promotion". Value can be "all" or a specific offer ID.
{ "name": "Promo Banners", "version": "1.0.0", "fields": {
"promotions": { "type": "promotion", "label": "Promotions" }
}}| Property | Type | Description |
|---|---|---|
| title | string | Offer title |
| slug | string | URL slug |
| description | string | null | Offer description |
| coverImage | string | null | Banner image URL |
| video | string | null | Video URL |
| products | array | Product ID array |
| order | number | Display order |
| isActive | boolean | Active status |
{% for promo in promotions %}
<a href="/promotions/{{ promo.slug }}" class="relative block rounded-xl overflow-hidden">
{% if promo.coverImage %}<img src="{{ promo.coverImage }}" alt="{{ promo.title }}" class="w-full h-64 object-cover">{% endif %}
<div class="absolute inset-0 bg-gradient-to-t from-black/70 flex items-end p-6">
<div><h2 class="text-2xl font-bold text-white">{{ promo.title }}</h2>
{% if promo.description %}<p class="text-white/80 mt-1 text-sm">{{ promo.description }}</p>{% endif %}</div>
</div>
</a>
{% endfor %}promotional-products
Single promotional offer with products populated as full objects. Config type: "promotional-products". Supports "automatic" on /promotions/:slug pages.
{ "name": "Promo Detail", "version": "1.0.0", "fields": {
"promotion": { "type": "promotional-products", "label": "Promotion" }
}}{% if promotion %}
<div class="text-center py-8">
<h1 class="text-3xl font-bold">{{ promotion.title }}</h1>
{% if promotion.description %}<p class="text-gray-500 mt-2 max-w-xl mx-auto">{{ promotion.description }}</p>{% endif %}
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-6xl mx-auto">
{% for product in promotion.products %}{% render 'product-card', product: product %}{% endfor %}
</div>
{% endif %}featured-products
Array of featured product sections. Config type: "featured-products". Value: "all" or a specific section ID.
{ "name": "Featured Sections", "version": "1.0.0", "fields": {
"featuredProducts": { "type": "featured-products", "label": "Featured Sections" }
}}| Property | Type | Description |
|---|---|---|
| title | string | Section title |
| slug | string | null | URL slug |
| mode | string | "manual" or "automatic" |
| automaticType | string | null | "top_selling", "discounted", "most_popular", "category", "brand" |
| limit | number | Max products |
| products | array | Product objects |
| image1, image2, image3 | string | null | Section banner images |
| order | number | Display order |
{% for section in featuredProducts %}
<section class="py-8 max-w-6xl mx-auto px-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-2xl font-bold">{{ section.title }}</h2>
{% if section.slug %}<a href="/featured/{{ section.slug }}" class="text-sm" style="color:{{ shop.brandColor }};">View All →</a>{% endif %}
</div>
{% if section.image1 %}<img src="{{ section.image1 }}" class="w-full rounded-lg mb-4" loading="lazy">{% endif %}
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{% for product in section.products %}{% render 'product-card', product: product %}{% endfor %}
</div>
</section>
{% endfor %}featured-section (Single)
Single featured section. Config type: "featured-section". Supports "automatic" on /featured/:slug pages or a specific slug/ID.
{ "name": "Featured Detail", "version": "1.0.0", "fields": {
"featured": { "type": "featured-section", "label": "Featured Section" }
}}{% if featured %}
<div class="max-w-6xl mx-auto py-8 px-4">
<h1 class="text-3xl font-bold">{{ featured.title }}</h1>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
{% for product in featured.products %}{% render 'product-card', product: product %}{% endfor %}
</div>
</div>
{% endif %}faqs
Array of active FAQs. Config: "type": "api", "apiType": "faqs".
{ "name": "FAQ Section", "version": "1.0.0", "fields": {
"title": { "type": "text", "label": "Title", "default": "Frequently Asked Questions" },
"faqs": { "type": "api", "apiType": "faqs", "label": "FAQs" }
}}| Property | Type | Description |
|---|---|---|
| question | string | FAQ question |
| answer | string | FAQ answer (may contain HTML) |
| order | number | Display order |
<section class="max-w-3xl mx-auto py-12 px-4">
<h2 class="text-2xl font-bold mb-6 text-center">{{ title }}</h2>
{% for faq in faqs %}
<details class="border-b py-4 group">
<summary class="cursor-pointer font-medium flex justify-between items-center">
{{ faq.question }}
<span class="text-gray-400 group-open:rotate-45 transition-transform text-xl">+</span>
</summary>
<div class="mt-3 text-gray-600 text-sm prose prose-sm">{{ faq.answer }}</div>
</details>
{% endfor %}
</section>branches
Array of shop branches. Config: "type": "api", "apiType": "branches". Same data as shop.branches.
{ "name": "Store Locator", "version": "1.0.0", "fields": {
"branches": { "type": "api", "apiType": "branches", "label": "Branches" }
}}<div class="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto py-8 px-4">
{% for branch in branches %}
<div class="border rounded-lg p-6">
<div class="flex items-center gap-2">
<h3 class="font-bold text-lg">{{ branch.name }}</h3>
{% if branch.isMainBranch %}<span class="text-[10px] bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">Main</span>{% endif %}
</div>
<p class="text-gray-600 mt-2">{{ branch.location }}</p>
{% if branch.googleMapLink %}<a href="{{ branch.googleMapLink }}" target="_blank" class="text-blue-500 text-sm mt-2 inline-block">View on Maps →</a>{% endif %}
</div>
{% endfor %}
</div>blog (Single Article)
Single shop article. Config type: "blog". Supports "automatic" on /blogs/:slug pages or a specific slug.
{ "name": "Blog Article", "version": "1.0.0", "fields": {
"article": { "type": "blog", "label": "Article" }
}}| Property | Type | Description |
|---|---|---|
| title | string | Article title |
| shortDesc | string | Brief excerpt |
| content | string | Full content (HTML) |
| coverImage | string | null | Cover image URL |
| slug | string | URL slug |
| tags | string[] | Tag strings |
| seoTitle | string | null | SEO title |
| seoDescription | string | null | SEO description |
| publishedAt | string | Publish date (ISO string) |
{% if article %}
<article class="max-w-3xl mx-auto py-8 px-4">
{% if article.coverImage %}<img src="{{ article.coverImage }}" alt="{{ article.title }}" class="w-full rounded-xl mb-6">{% endif %}
<h1 class="text-4xl font-bold">{{ article.title }}</h1>
<p class="text-gray-500 mt-2">{{ article.shortDesc }}</p>
<div class="flex flex-wrap gap-2 mt-4">
{% for tag in article.tags %}<span class="bg-gray-100 text-xs px-3 py-1 rounded-full">{{ tag }}</span>{% endfor %}
</div>
<div class="prose prose-lg mt-8 max-w-none">{{ article.content }}</div>
</article>
{% endif %}blog-list
Paginated list of published articles. Config type: "blog-list".
{ "name": "Blog List", "version": "1.0.0", "fields": {
"blogList": { "type": "blog-list", "label": "Blog Articles" }
}}| Property | Type | Description |
|---|---|---|
| articles | array | Article objects (same as single blog) |
| total | number | Total articles count |
| totalPages | number | Total pages |
| page | number | Current page number |
| limit | number | Items per page |
| search | string | undefined | Current search query |
<div class="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto py-8 px-4">
{% for article in blogList.articles %}
<a href="/blogs/{{ article.slug }}" class="block border rounded-lg overflow-hidden hover:shadow-md transition">
{% if article.coverImage %}<img src="{{ article.coverImage }}" alt="{{ article.title }}" class="w-full h-48 object-cover">{% endif %}
<div class="p-4">
<h3 class="font-semibold line-clamp-2">{{ article.title }}</h3>
<p class="text-sm text-gray-500 mt-1 line-clamp-2">{{ article.shortDesc }}</p>
</div>
</a>
{% endfor %}
</div>
{% if blogList.totalPages > 1 %}
<p class="text-center text-sm text-gray-400 pb-8">Page {{ blogList.page }} of {{ blogList.totalPages }} ({{ blogList.total }} articles)</p>
{% endif %}order-details
Order object for tracking pages. Config: "type": "api", "apiType": "order-details". Typically via page context.
{ "name": "Order Status", "version": "1.0.0", "fields": {
"order": { "type": "api", "apiType": "order-details", "label": "Order" }
}}| Property | Type | Description |
|---|---|---|
| orderNumber | string | Unique order number |
| status | string | "pending", "confirmed", "approved", "packaging", "shipping", "shipped", "delivering", "delivered", "canceled" |
| fullName | string | Customer name |
| string | null | Customer email | |
| mobileNumber | string | Customer phone |
| city | string | null | Delivery city |
| address | string | null | Delivery address |
| items | array | Line items: { productName, variantName, quantity, price, totalPrice, extraFields } |
| subtotal | number | Subtotal |
| tax | number | Tax |
| shippingCost | number | Shipping cost |
| discount | number | Discount |
| totalAmount | number | Final total |
| paymentMethod | string | Payment method |
| paidStatus | string | "unpaid", "paid", "partial" |
| trackingUrl | string | null | Tracking URL |
| trackingNumber | string | null | Tracking number |
| promocode | string | null | Applied promo code |
| statusHistory | array | Array of { status, changedAt } |
| notes | string | null | Order notes |
| createdAt | string | Order date (ISO) |
{% if order %}
<div class="max-w-2xl mx-auto py-8 px-4">
<h1 class="text-2xl font-bold">Order #{{ order.orderNumber }}</h1>
<div class="flex gap-2 mt-2">
<span class="px-3 py-1 rounded-full text-xs font-medium
{% if order.status == 'delivered' %}bg-green-100 text-green-700
{% elsif order.status == 'canceled' %}bg-red-100 text-red-700
{% else %}bg-blue-100 text-blue-700{% endif %}">{{ order.status | capitalize }}</span>
<span class="px-3 py-1 rounded-full text-xs font-medium
{% if order.paidStatus == 'paid' %}bg-green-100 text-green-700
{% else %}bg-yellow-100 text-yellow-700{% endif %}">{{ order.paidStatus | capitalize }}</span>
</div>
<table class="w-full text-sm mt-6">
<thead><tr class="border-b text-left"><th class="py-2">Product</th><th class="py-2 text-right">Qty</th><th class="py-2 text-right">Price</th><th class="py-2 text-right">Total</th></tr></thead>
<tbody>{% for item in order.items %}
<tr class="border-b"><td class="py-2">{{ item.productName }}{% if item.variantName %}<br><small class="text-gray-400">{{ item.variantName }}</small>{% endif %}</td>
<td class="py-2 text-right">{{ item.quantity }}</td><td class="py-2 text-right">{{ shop.priceUnit }} {{ item.price }}</td><td class="py-2 text-right">{{ shop.priceUnit }} {{ item.totalPrice }}</td></tr>
{% endfor %}</tbody>
</table>
<div class="mt-4 text-right text-sm space-y-1">
<div>Subtotal: {{ shop.priceUnit }} {{ order.subtotal }}</div>
{% if order.tax > 0 %}<div>Tax: {{ shop.priceUnit }} {{ order.tax }}</div>{% endif %}
{% if order.shippingCost > 0 %}<div>Shipping: {{ shop.priceUnit }} {{ order.shippingCost }}</div>{% endif %}
{% if order.discount > 0 %}<div class="text-green-600">Discount: -{{ shop.priceUnit }} {{ order.discount }}</div>{% endif %}
<div class="text-lg font-bold">Total: {{ shop.priceUnit }} {{ order.totalAmount }}</div>
</div>
<div class="mt-6 text-sm"><p class="font-medium">{{ order.fullName }}</p><p class="text-gray-500">{{ order.address }}{% if order.city %}, {{ order.city }}{% endif %}</p><p class="text-gray-500">{{ order.mobileNumber }}</p></div>
{% if order.trackingUrl %}<a href="{{ order.trackingUrl }}" target="_blank" class="mt-4 inline-block px-4 py-2 rounded text-white text-sm" style="background:{{ shop.brandColor }};">Track Shipment</a>{% endif %}
</div>
{% endif %}product-ratings
Product review ratings. Config: "type": "api", "apiType": "product-ratings".
{ "name": "Reviews", "version": "1.0.0", "fields": {
"productRatings": { "type": "api", "apiType": "product-ratings", "label": "Ratings" }
}}| Property | Type | Description |
|---|---|---|
| rating | number | Rating 1–5 |
| name | string | Reviewer name |
| review | string | null | Review text |
| createdAt | string | Review date (ISO) |
{% for r in productRatings %}
<div class="border-b py-4">
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{{ r.name }}</span>
<span class="text-yellow-500 text-sm">{% for i in (1..5) %}{% if i <= r.rating %}★{% else %}☆{% endif %}{% endfor %}</span>
</div>
{% if r.review %}<p class="text-gray-600 text-sm mt-1">{{ r.review }}</p>{% endif %}
</div>
{% endfor %}searchData
Auto-available on search pages via page context. Config type: "searchData".
{ "name": "Search Results", "version": "1.0.0", "fields": {
"searchData": { "type": "searchData", "label": "Search Data" }
}}| Property | Type | Description |
|---|---|---|
| q | string | undefined | Search query |
| brandSlug | string | undefined | Brand filter slug |
| categorySlug | string | undefined | Category filter slug |
| subcategorySlug | string | undefined | Subcategory filter slug |
| sortBy | string | Sort field |
| sortOrder | string | "asc" or "desc" |
| products | array | Matching product objects |
<section class="max-w-6xl mx-auto py-8 px-4">
{% if searchData.q %}<h1 class="text-2xl font-bold">Results for “{{ searchData.q }}”</h1>
{% else %}<h1 class="text-2xl font-bold">All Products</h1>{% endif %}
{% if searchData.products.size > 0 %}
<p class="text-sm text-gray-500 mt-1">{{ searchData.products.size }} products found</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
{% for product in searchData.products %}{% render 'product-card', product: product %}{% endfor %}
</div>
{% else %}
<div class="text-center py-16"><p class="text-gray-400 text-lg">No products found.</p><a href="/" class="text-blue-500 mt-2 inline-block">Back to Home</a></div>
{% endif %}
</section>Config Field Types
Complete reference of all field types for config.json. The field key becomes the variable name in your Liquid template.
Static / Editor Fields
These fields are set by the user in the visual editor and passed directly to the template.
| Type | Widget | Liquid Value |
|---|---|---|
| text / string | Text input | string |
| textarea | Multi-line text | string |
| number | Number input | number |
| boolean | Toggle switch | true / false |
| color | Color picker | hex string ("#ff0000") |
| date | Date picker | date string |
| time | Time picker | time string |
| url | URL input | URL string |
| option | Dropdown select | selected value string |
| image | Image uploader | image URL string |
| icon | Icon picker | icon path (auto-converted to full URL) |
| toggle | Toggle selector | string value |
| tabs | Tab selector | string value |
| alignment | Alignment picker | "left", "center", "right" |
| text-align | Text alignment | "left", "center", "right", "justify" |
| font-style | Font style picker | string |
| padding | Padding controls | CSS value string |
| border-radius | Border radius | CSS value string |
| size | Size selector | string |
| code | Code editor | raw HTML/CSS string |
| wysiwyg | Rich text editor | HTML string |
| array_object | Repeatable items | array or { items: [...] } |
API Data Fields Summary
| Config Type | Mode | Returns |
|---|---|---|
| products | Always fetches | Array of product objects (max 100) |
| product | "automatic" or slug | Single product or null |
| api { apiType: "categories" } | Always fetches | Array of categories |
| categories-expanded | Always fetches | Categories + subcategories + products nested |
| api { apiType: "brands" } | Always fetches | { brands: [...] } (note: nested!) |
| promotion | "all" or ID | Array of promotional offers |
| promotional-products | "automatic" or slug | Offer with populated products |
| featured-products | "all" or ID | Array of featured sections |
| featured-section | "automatic" or slug | Single featured section or null |
| api { apiType: "faqs" } | Always fetches | Array of FAQs |
| api { apiType: "branches" } | Always fetches | Array of branches |
| blog | "automatic" or slug | Single article or null |
| blog-list | Always fetches | { articles, total, totalPages, page, limit } |
| api { apiType: "order-details" } | Page context | Order object |
| api { apiType: "product-ratings" } | Page context / fetch | Array of ratings |
| searchData | Page context | { q, products, filters... } |
Conditional Fetching
API fields can have a condition to only fetch when another field matches a value. Saves unnecessary API calls.
{
"name": "Flexible Section",
"version": "1.0.0",
"fields": {
"showBrands": { "type": "boolean", "label": "Show Brands", "default": false },
"brands": {
"type": "api",
"apiType": "brands",
"label": "Brands",
"condition": { "field": "showBrands", "value": true }
},
"showFaqs": { "type": "boolean", "label": "Show FAQs", "default": false },
"faqs": {
"type": "api",
"apiType": "faqs",
"condition": { "field": "showFaqs", "value": true }
}
}
}{% if showBrands and brands %}
<h2>Our Brands</h2>
<div class="flex flex-wrap gap-4">
{% for brand in brands.brands %}<span class="px-3 py-1 border rounded">{{ brand.name }}</span>{% endfor %}
</div>
{% endif %}
{% if showFaqs and faqs %}
<h2 class="mt-8">FAQs</h2>
{% for faq in faqs %}<details class="border-b py-3"><summary class="cursor-pointer font-medium">{{ faq.question }}</summary><p class="mt-2 text-sm text-gray-600">{{ faq.answer }}</p></details>{% endfor %}
{% endif %}Array Object Fields (Repeatable Items)
Use "array_object" or "array-object" for repeatable user-defined lists (slides, testimonials, links, etc.).
{
"name": "Testimonials",
"version": "1.0.0",
"fields": {
"testimonials": {
"type": "array_object",
"label": "Testimonials",
"fields": {
"name": { "type": "string", "label": "Name", "placeholder": "Customer name" },
"quote": { "type": "textarea", "label": "Quote" },
"avatar": { "type": "image", "label": "Photo" },
"rating": { "type": "number", "label": "Stars (1-5)", "default": 5 }
},
"value": [
{ "name": "John Doe", "quote": "Amazing product!", "avatar": "", "rating": 5 }
]
}
}
}items wrapper
If your config declares"value": { "items": [...] }, the data is wrapped as { items: [...] }. If it's a flat array, you iterate directly. Check how you define value.<div class="grid md:grid-cols-3 gap-6">
{% for t in testimonials %}
<div class="border rounded-lg p-6 text-center">
{% if t.avatar %}<img src="{{ t.avatar }}" class="w-16 h-16 rounded-full mx-auto mb-3 object-cover">{% endif %}
<p class="text-sm italic text-gray-600">“{{ t.quote }}”</p>
<p class="font-semibold text-sm mt-3">{{ t.name }}</p>
<div class="text-yellow-500 text-sm mt-1">{% for i in (1..5) %}{% if i <= t.rating %}★{% else %}☆{% endif %}{% endfor %}</div>
</div>
{% endfor %}
</div>Option / Tabs Fields
Dropdowns and tab selectors let users pick from predefined values.
{
"fields": {
"layout": {
"type": "option",
"label": "Layout Style",
"default": "grid",
"options": [
{ "label": "Grid", "value": "grid" },
{ "label": "List", "value": "list" },
{ "label": "Carousel", "value": "carousel" }
]
},
"theme": {
"type": "tabs",
"label": "Theme",
"default": "light",
"options": [
{ "label": "Light", "value": "light" },
{ "label": "Dark", "value": "dark" }
]
}
}
}{% if layout == "grid" %}
<div class="grid grid-cols-4 gap-4">...</div>
{% elsif layout == "list" %}
<div class="space-y-4">...</div>
{% elsif layout == "carousel" %}
<div class="flex overflow-x-auto gap-4">...</div>
{% endif %}
<section class="{% if theme == 'dark' %}bg-gray-900 text-white{% else %}bg-white text-gray-900{% endif %} py-8">
...
</section>Custom Liquid Filters
In addition to all standard LiquidJS filters, these custom filters are registered:
| Filter | Usage | Description |
|---|---|---|
| inline_svg | {{ path | inline_svg }} | Reads SVG file, returns raw SVG HTML. Falls back to a placeholder on error. |
| svg_class | {{ svg | svg_class: "w-5 h-5" }} | Adds CSS classes to an SVG element |
| svg_size | {{ svg | svg_size: 24 }} | Sets width and height attributes on SVG |
{%- comment -%} Chain: inline SVG, add Tailwind classes, set dimensions {%- endcomment -%}
{{ iconPath | inline_svg | svg_class: "w-6 h-6 text-gray-500" | svg_size: 24 }}
{%- comment -%} Use an icon field value {%- endcomment -%}
{% if slide.icon %}
<span class="w-6 h-6 shrink-0 inline-block"
style="background:var(--brand-color);
-webkit-mask-image:url({{ slide.icon }});mask-image:url({{ slide.icon }});
-webkit-mask-size:contain;mask-size:contain;
-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
-webkit-mask-position:center;mask-position:center;">
</span>
{% endif %}Auto icon URL conversion
All values starting with/assets/icons/ are automatically converted to full backend URLs (e.g. http://backend:4000/assets/icons/heart.svg) before template rendering. You don't need to prepend the backend URL yourself.Rendering Limits
To prevent infinite loops and abuse, the LiquidJS engine enforces strict DoS limits on every render:
| Limit | Value | Description |
|---|---|---|
| parseLimit | 1 MB | Maximum template source size |
| renderLimit | 10 seconds | Maximum render time before timeout |
| memoryLimit | 10 million | Maximum object allocations during render |
Exceeding limits
If a template exceeds these limits, it returns empty HTML and logs an error. Deeply nested loops, recursive renders, or very large data sets can trigger this. Keep templates lean and uselimit on {% for %} loops.Liquid Syntax Reference
Variables & Filters
{%- comment -%} Output {%- endcomment -%}
{{ shop.shopName }}
{{ product.price }}
{{ title | default: "Untitled" }}
{%- comment -%} String filters {%- endcomment -%}
{{ "hello" | upcase }} => HELLO
{{ "HELLO" | downcase }} => hello
{{ "hello world" | capitalize }} => Hello world
{{ "Hello World" | truncate: 10 }} => Hello W...
{{ "Hello World" | truncatewords: 1 }} => Hello...
{{ product.name | slice: 0, 1 | upcase }} => first letter uppercase
{{ product.description | strip_html }} => plain text
{{ " hello " | strip }} => "hello"
{{ "hello-world" | split: "-" }} => ["hello", "world"]
{{ "hello" | prepend: "say " }} => "say hello"
{{ "hello" | append: " world" }} => "hello world"
{{ "hello" | replace: "hello", "hi" }} => "hi"
{%- comment -%} Number filters {%- endcomment -%}
{{ 4.56 | round }} => 5
{{ 4.56 | round: 1 }} => 4.6
{{ 100 | plus: 13 }} => 113
{{ 100 | minus: 13 }} => 87
{{ 10 | times: 1.13 }} => 11.3
{{ 100 | divided_by: 3 }} => 33
{{ 100 | modulo: 3 }} => 1
{%- comment -%} Array filters {%- endcomment -%}
{{ products | size }} => count
{{ products | first }} => first item
{{ products | last }} => last item
{{ tags | join: ", " }} => "tag1, tag2"
{{ tags | sort }} => alphabetical
{{ tags | uniq }} => deduplicated
{{ tags | reverse }} => reversed
{{ products | where: "freeDelivery", true }} => filter array
{%- comment -%} Date filters {%- endcomment -%}
{{ product.createdAt | date: "%B %d, %Y" }} => "January 15, 2026"
{{ "now" | date: "%Y" }} => current yearConditionals
{%- comment -%} if / elsif / else {%- endcomment -%}
{% if product.compareAtPrice and product.compareAtPrice > product.price %}
<span class="badge-sale">On Sale!</span>
{% elsif product.freeDelivery %}
<span class="badge-free">Free Delivery</span>
{% else %}
<span>{{ shop.priceUnit }} {{ product.price }}</span>
{% endif %}
{%- comment -%} unless (negation of if) {%- endcomment -%}
{% unless product.quantity == 0 %}
<button>Add to Cart</button>
{% endunless %}
{%- comment -%} case / when {%- endcomment -%}
{% case order.status %}
{% when "delivered" %}<span class="text-green-600">Delivered</span>
{% when "canceled" %}<span class="text-red-600">Canceled</span>
{% else %}<span class="text-blue-600">{{ order.status | capitalize }}</span>
{% endcase %}
{%- comment -%} Operators: == != > < >= <= or and contains {%- endcomment -%}
{% if product.tags contains "new" %}
<span class="badge">New Arrival</span>
{% endif %}
{%- comment -%} Blank check (nil, empty string, empty array) {%- endcomment -%}
{% if shop.shopLogo != blank %}
<img src="{{ shop.shopLogo }}" alt="{{ shop.shopName }}">
{% endif %}
{% unless description == blank %}
<div class="prose">{{ description }}</div>
{% endunless %}Loops
{%- comment -%} Basic {%- endcomment -%}
{% for product in products %}
<div>{{ product.name }}</div>
{% endfor %}
{%- comment -%} With limit and offset {%- endcomment -%}
{% for product in products limit: 4 %}...{% endfor %}
{% for product in products limit: 4 offset: 4 %}...{% endfor %}
{%- comment -%} Loop variables {%- endcomment -%}
{% for product in products %}
{{ forloop.index }} => 1, 2, 3, ...
{{ forloop.index0 }} => 0, 1, 2, ...
{{ forloop.first }} => true on first
{{ forloop.last }} => true on last
{{ forloop.length }} => total count
{{ forloop.rindex }} => reverse: 3, 2, 1
{% endfor %}
{%- comment -%} Empty fallback (else) {%- endcomment -%}
{% for product in products %}
<div>{{ product.name }}</div>
{% else %}
<p>No products found.</p>
{% endfor %}
{%- comment -%} Range loop {%- endcomment -%}
{% for i in (1..5) %}
<span>Star {{ i }}</span>
{% endfor %}
{%- comment -%} break and continue {%- endcomment -%}
{% for product in products %}
{% if product.quantity == 0 %}{% continue %}{% endif %}
{% if forloop.index > 10 %}{% break %}{% endif %}
<div>{{ product.name }}</div>
{% endfor %}Render Partials
{% render %} includes other components as partials. Parent scope variables are available in rendered templates.
{%- comment -%} Render product card, passing product variable {%- endcomment -%}
{% render 'product-card', product: product %}
{%- comment -%} Inside a loop {%- endcomment -%}
{% for product in products %}
{% render 'product-card', product: product %}
{% endfor %}
{%- comment -%} Render layout partials {%- endcomment -%}
{% render 'header' %}
{% render 'footer' %}
{%- comment -%} Pass multiple variables {%- endcomment -%}
{% render 'promo-badge', text: "SALE", color: "#ff0000" %}Parent scope sharing
In LiquidJS, when you use{% render %}, the parent scope variables (like borderRadius, shop, etc.) are available in the rendered partial. You only need to explicitly pass variables that are loop-specific or renamed.Assign & Capture
{%- comment -%} Assign a variable {%- endcomment -%}
{% assign discountPercent = product.compareAtPrice | minus: product.price | times: 100 | divided_by: product.compareAtPrice %}
<span>{{ discountPercent }}% off</span>
{%- comment -%} Assign with default {%- endcomment -%}
{% assign heroTitle = title | default: "Welcome to " | append: shop.shopName %}
{%- comment -%} Capture multi-line content into a variable {%- endcomment -%}
{% capture priceHtml %}
<span class="font-bold">{{ shop.priceUnit }} {{ product.price }}</span>
{% if product.compareAtPrice > product.price %}
<span class="line-through text-gray-400 text-sm">{{ shop.priceUnit }} {{ product.compareAtPrice }}</span>
{% endif %}
{% endcapture %}
{%- comment -%} Use it later {%- endcomment -%}
<div class="price-display">{{ priceHtml }}</div>
{%- comment -%} Increment / Decrement (independent counters) {%- endcomment -%}
{% increment myCounter %} => 0
{% increment myCounter %} => 1
{% increment myCounter %} => 2Whitespace Control
Use {%- and -%} (with hyphens) to strip whitespace around Liquid tags. Useful for keeping clean HTML output.
{%- comment -%} Without whitespace control: adds blank lines {%- endcomment -%}
{% if product.freeDelivery %}
Free Delivery
{% endif %}
{%- comment -%} With whitespace control: clean output {%- endcomment -%}
{%- if product.freeDelivery -%}
Free Delivery
{%- endif -%}
{%- comment -%} Raw: escape all Liquid syntax {%- endcomment -%}
{% raw %}
This {{ will not }} be parsed. Useful for showing Liquid code examples.
{% endraw %}
{%- comment -%} Comments are invisible in output {%- endcomment -%}
{% comment %}
This entire block is hidden from the rendered HTML.
Use it for notes, TODOs, or documentation.
{% endcomment %}Page Contexts
Certain pages auto-inject data. Components using "automatic" mode receive this data.
| Page URL | Variable | Description |
|---|---|---|
| /product/:slug | product | Full product object with relations |
| /promotions/:slug | promotion | Promotional offer with populated products |
| /featured/:slug | featured | Featured section with products |
| /order-status/:id | order | Order details with items and status history |
| /search?q=... | searchData | Search query, filters, and matching products |
| /blogs/:slug | article | Blog article with full HTML content |
| /blogs | blogData | Paginated blog list with search/pagination |
| (review pages) | productRatings | Rating data for the product |
Automatic vs. Manual
When a field is"automatic", it receives the page context data. On pages without that context, the value is null. Setting a specific slug always fetches that item regardless of the current page.Real-World Examples
Production-ready component examples showing real config + liquid patterns used on live stores.
Product Card Block
This is the reusable product card block that other components render via {% render 'product-card', product: product %}. It uses the global block variables for customization.
{
"name": "Product Card",
"version": "1.0.0",
"description": "Reusable product card block",
"fields": {}
}<a href="/products/{{ product.slug }}" class="group block border rounded-lg overflow-hidden hover:shadow-md transition"
style="border-radius: {{ borderRadius | default: '8px' }};">
<div class="aspect-square bg-gray-50 overflow-hidden" style="border-radius: {{ imageRounding | default: '0' }};">
{% if product.coverImage %}
<img src="{{ product.coverImage }}" alt="{{ product.name }}"
class="w-full h-full object-cover {% if hoverEffect == 'scale' %}group-hover:scale-105{% endif %} transition-transform duration-300"
loading="lazy">
{% else %}
<div class="w-full h-full flex items-center justify-center text-gray-300 text-2xl font-bold">
{{ product.name | slice: 0, 1 | upcase }}
</div>
{% endif %}
</div>
<div style="padding: {{ cardPadding | default: '12px' }};">
<h3 style="font-size: {{ titleSize | default: '14px' }};" class="font-medium line-clamp-2 text-gray-800 group-hover:text-gray-900">
{{ product.name }}
</h3>
<div class="flex items-center gap-2 mt-1.5">
{% if product.compareAtPrice and product.compareAtPrice > product.price %}
<span class="text-xs line-through text-gray-400">{{ shop.priceUnit }} {{ product.compareAtPrice }}</span>
{% endif %}
<span class="font-bold text-sm" style="color: {{ shop.brandColor }};">{{ shop.priceUnit }} {{ product.price }}</span>
</div>
{% if product.avgReviews > 0 %}
<div class="text-xs text-yellow-500 mt-1">
{% for i in (1..5) %}{% if i <= product.avgReviews %}★{% else %}☆{% endif %}{% endfor %}
<span class="text-gray-400 ml-1">({{ product.reviewsCount }})</span>
</div>
{% endif %}
{% if product.quantity == 0 and product.unlimitedStock == false %}
<span class="text-[10px] text-red-500 mt-1 block">Out of Stock</span>
{% elsif product.freeDelivery %}
<span class="text-[10px] text-green-600 mt-1 block">Free Delivery</span>
{% endif %}
{% if showAddToCart %}
<button class="{{ buttonStyle | default: '' }} mt-3 w-full py-2 rounded text-sm font-medium transition-colors"
style="background: {{ shop.brandColor }}; color: {{ shop.textOverBrandColor }};">
Add to Cart
</button>
{% endif %}
</div>
</a>Best Practices
Performance
- Always add
loading="lazy"to images inside loops to defer off-screen images - Use
{% for product in products limit: 8 %}to cap rendered items instead of looping all 100 - Avoid deeply nested loops (e.g. products inside categories inside sections) — they multiply render cost
- Keep
<script>blocks minimal. Use event delegation instead of per-elementonclickhandlers for large lists - Use conditional fetching (
conditionin config) to avoid loading data that won't be rendered - The 10-second render limit means your template should render fast — avoid computing heavy values in Liquid
Responsive Design
- Use Tailwind responsive prefixes:
grid-cols-2 md:grid-cols-3 lg:grid-cols-4 - Always test mobile layout — use the viewport switcher in the visual editor preview
- Use
hidden lg:block/lg:hiddento show/hide elements by breakpoint (e.g. mobile menu vs desktop nav) - Images should use
class="w-full h-auto"oraspect-square object-coverfor consistent sizing - Keep touch targets at least 44x44px on mobile (buttons, links)
SEO
- Always include
altattributes on images:alt="{{ product.name }}" - Use semantic HTML:
<header>,<nav>,<main>,<article>,<footer> - Use heading hierarchy: one
<h1>per page, then<h2>,<h3>etc. - Add
rel="noopener noreferrer"andtarget="_blank"on external links - Use
line-clamp-2for truncation instead of| truncate(preserves full text for search engines) - Product descriptions should use
<div class="prose">for proper content styling
Liquid Template API Reference — Zalient Ecommerce Platform
Built with LiquidJS · Tailwind CSS supported
