HomePricingTemplatesBlogs
Download appStart free
← Back to Develop

Liquid Template API

Developer reference for storefront components

0%
Overview
How It WorksFile StructureData FlowDefault Template
shop Object
socialMediaLinksfooterQuickLinksbranchesPoliciesPayments
component Object
Block Variables
products
VariantsSpecificationsExtra FieldsAdd-onsFeatures
product (single)
categories
categories-expanded
Direct Products
brands
promotions
promotional-products
featured-products
featured-section
faqs
branches
blog (single)
blog-list
order-details
product-ratings
searchData
Config Field Types
Static FieldsAPI FieldsConditional FetchingArray ObjectsOption / Tabs
Custom Filters
Rendering Limits
Liquid Syntax
Variables & FiltersConditionalsLoopsRender PartialsAssign & CaptureWhitespace
Page Contexts
Real-World Examples
Mega Menu HeaderFooterProduct Card
Best Practices
PerformanceResponsiveSEO
DevelopLiquid Template API

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

  1. The rendering engine reads config.json and inspects the fields object
  2. API fields (products, categories, brands, etc.) are fetched automatically from the backend based on their type / apiType
  3. Static fields (text, color, number, boolean, image, etc.) are pulled from the visual editor's saved data
  4. Conditional fields with a condition property are only fetched when the condition is met
  5. All data is merged into one context: { shop, component, ...blockVars, ...staticFields, ...apiData }
  6. Icon paths starting with /assets/icons/ are auto-converted to full backend URLs
  7. The context is passed to LiquidJS which renders the .liquid template 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.

config.jsonjson
{
  "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"
    }
  }
}
my-component.liquidliquid
<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 browser

Default Component Template

When you create a new component in the developer panel, it starts with this default:

config.json (default)json
{
  "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"
    }
  }
}
my-component.liquid (default)liquid
<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.

PropertyTypeDefaultDescription
shop._idstring—MongoDB shop ID
shop.slugstring""Shop subdomain slug (e.g. "my-store")
shop.shopNamestring""Store display name
shop.shopLogostring | nullnullLogo image URL
shop.shopDescriptionstring""Store description (max 1000 chars)
shop.locationstring""Store location / primary address
shop.publicEmailstring""Public contact email
shop.publicContactNumberstring""Public phone number
shop.priceUnitstring"Rs"Currency symbol: "Rs", "$", "USD", etc.
shop.brandColorstring"#3b82f6"Primary brand color (hex)
shop.textOverBrandColorstring"#FFFFFF"Text color to use over brand color
shop.brandFontstring"Inter"Brand font family name
shop.socialMediaLinksarray[]Social media links (see below)
shop.footerLogostring | nullnullFooter logo URL (can differ from main)
shop.footerQuickLinksarray[]Footer navigation links (see below)
shop.panVatNumberstring | nullnullPAN/VAT registration number
shop.ecommerceRegNumberstring | nullnullEcommerce platform registration ID
shop.privacyPolicystring | nullnullPrivacy policy (HTML)
shop.termsAndConditionsstring | nullnullTerms & conditions (HTML)
shop.returnPolicystring | nullnullReturn policy (HTML)
shop.refundPolicystring | nullnullRefund policy (HTML)
shop.aboutstring | nullnullAbout page (HTML)
shop.branchesarray[]Store branches (see below)
shop.paymentMethodsarray[]Payment method name strings
shop.paymentMethodConfigobject{}Payment method config (internal)

shop.socialMediaLinks[]

PropertyTypeDefaultDescription
urlstring—Social media profile URL
titlestring—"facebook", "instagram", "whatsapp", "twitter", "tiktok", "youtube", "linkedin", "threads"
social-links.liquidliquid
<div class="flex items-center gap-4">
  {% for link in shop.socialMediaLinks %}
    <a href="{{ link.url }}" target="_blank" rel="noopener noreferrer"
       class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center hover:opacity-80">
      {{ link.title | capitalize }}
    </a>
  {% endfor %}
</div>

shop.footerQuickLinks[]

PropertyTypeDefaultDescription
titlestring—Link display text
urlstring—Link URL (relative or absolute)
footer-links.liquidliquid
<div class="space-y-2">
  <h4 class="font-semibold text-sm">Quick Links</h4>
  {% for link in shop.footerQuickLinks %}
    <a href="{{ link.url }}" class="block text-sm text-gray-500 hover:text-gray-700">{{ link.title }}</a>
  {% endfor %}
</div>

shop.branches[]

PropertyTypeDefaultDescription
namestring—Branch name
locationstring—Branch address
googleMapLinkstring—Google Maps embed or link URL
isMainBranchboolean—Whether this is the main/HQ branch
branches-map.liquidliquid
{% 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 &rarr;</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.

policy-page.liquidliquid
{% 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"].

payment-badges.liquidliquid
<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

config.jsonjson
{
  "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": "" }
  }
}
header-1.liquidliquid
{% 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.

PropertyTypeDefaultDescription
component.pathstring—Component path, e.g. "hero/hero-1", "products/product-grid-1"
debug-info.liquidliquid
{%- 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.

PropertyTypeDefaultDescription
borderRadiusstring—Card border radius CSS value (e.g. "8px", "0.5rem")
imageRoundingstring—Product image border radius
hoverEffectstring—Hover animation type (e.g. "scale", "shadow", "none")
showAddToCartboolean—Whether to show the Add to Cart button on cards
cardPaddingstring—Card inner padding CSS value
titleSizestring—Product title font size CSS value
buttonStylestring—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.
product-card.liquidliquid
<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".

config.jsonjson
{
  "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

PropertyTypeDefaultDescription
namestring—Product name (2-255 chars)
slugstring—URL-friendly identifier for routing
descriptionstring—Product description (may contain HTML, max 50000 chars)
pricenumber—Current selling price (min 0)
compareAtPricenumber | null—Original/strikethrough price for showing discounts
costPerItemnumber | null—Cost per item (internal, may be exposed)
coverImagestring | null—Main product image URL
imagesstring[]—Additional image URLs array
quantitynumber—Available stock quantity
unlimitedStockboolean—If true, stock is infinite (ignore quantity)
trackQuantityboolean—Whether stock tracking is enabled
allowBackorderboolean—Allow orders when out of stock
skustring | null—Stock keeping unit code
barcodestring | null—Product barcode
categoryIdstring—Category ID reference
subcategoryIdstring | null—Subcategory ID reference
brandIdstring | null—Brand ID reference
avgReviewsnumber—Average star rating (0–5, cached)
reviewsCountnumber—Total number of reviews (cached)
productSoldnumber—Total units sold (cached)
tagsstring[]—Array of tag strings
featuresarray—Array of feature objects (title, description, image)
featuresSectionTitlestring | null—Custom heading for features section
specificationsarray—Specification groups (see below)
extraFieldsarray—Custom field definitions (see below)
isPublishedboolean—Publication status
freeDeliveryboolean—Whether delivery is free
deliveryTimestring | null—Estimated delivery time text
weightnumber | null—Product weight value
weightUnitstring"kg""kg", "g", "lb", "oz"
isComboProductboolean—Whether this is a combo/bundle product
metaTitlestring | null—SEO meta title
metaDescriptionstring | null—SEO meta description
createdAtstring—Creation date (ISO string)
updatedAtstring—Last update date (ISO string)
product-grid.liquidliquid
<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 %}&#9733;{% else %}&#9734;{% 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.

PropertyTypeDefaultDescription
optionsobject—Map of option names to value arrays: { "Color": ["Red", "Blue"], "Size": ["S", "M", "L"] }
pricenumber | null—Variant-specific price (overrides product price if set)
compareAtPricenumber | null—Variant compare/original price
skustring | null—Variant-specific SKU
barcodestring | null—Variant barcode
quantitynumber—Variant stock count
coverImagestring | null—Variant-specific image (e.g. different color)
weightnumber | null—Variant weight
weightUnitstring"kg""kg", "g", "lb", "oz"
isActiveboolean—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.

specifications.liquidliquid
{% 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.

PropertyTypeDefaultDescription
labelstring—Field label shown to customer
fieldTypestring—"text", "textarea", or "imageUpload"
isRequiredboolean—Whether the customer must fill this
placeholderstring | null—Input placeholder text
extra-fields.liquidliquid
{% 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.

PropertyTypeDefaultDescription
_idstring—Add-on product ID
namestring—Add-on product name
slugstring—Slug used by the product details URL
coverImagestring | null—Main product image URL
pricenumber—Current selling price
compareAtPricenumber | null—Optional crossed-out original price
quantitynumber—Product quantity summary; variants are not included in this compact object
unlimitedStockboolean—Whether stock is unlimited
isPublishedboolean—Publication status; returned add-ons are published
isActiveboolean—Active status; returned add-ons are active
isDeletedboolean—Deletion status; always false for returned add-ons
regionPricingobject | 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.
product-add-ons.liquidliquid
{% 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.

PropertyTypeDefaultDescription
titlestring—Feature name
descriptionstring | null—Feature description
imagestring | null—Feature icon/image URL
product-features.liquidliquid
{% 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.

config.jsonjson
{
  "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:

PropertyTypeDefaultDescription
categoryobject | null—Populated category: { name, slug, image, description }
subcategoryobject | null—Populated subcategory: { name, slug, image }
brandobject | null—Populated brand: { name, slug, image }
similarProductsarray—Array of related products (same category)
addOnProductsarray—Merchant-selected add-on product summaries; separate from similarProducts and combo items
product-detail.liquidliquid
{% 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 %}&#9733;{% else %}&#9734;{% endif %}{% endfor %}</span>
        <span class="text-gray-400">({{ product.reviewsCount }} reviews)</span>
        <span class="text-gray-400">&middot; {{ 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">&#x2713; 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".

config.jsonjson
{
  "name": "Category Grid",
  "version": "1.0.0",
  "fields": {
    "categories": { "type": "api", "apiType": "categories", "label": "Categories" }
  }
}
PropertyTypeDefaultDescription
namestring—Category name
slugstring—URL-friendly identifier
descriptionstring | null—Category description
imagestring | null—Category image URL
isActiveboolean—Active status
ordernumber—Display order (lower = first)
category-grid.liquidliquid
<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.

config.jsonjson
{
  "name": "Mega Menu",
  "version": "1.0.0",
  "fields": {
    "categoryMenu": { "type": "categories-expanded", "label": "Category Navigation" }
  }
}
PropertyTypeDefaultDescription
namestring—Category name
slugstring—URL-friendly identifier
subcategoriesarray—Subcategories with products nested (see below)
productsarray—Products directly under this category (no subcategory)

Subcategory Object

PropertyTypeDefaultDescription
namestring—Subcategory name
slugstring—URL slug (or category slug for virtual "All")
productsarray—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:

PropertyTypeDefaultDescription
namestring—Product name (or parent product name if variant)
slugstring—Product slug for URL
parentProductNamestring | null—Parent product name (for child/variant products)
coverImagestring | null—Product cover image URL
mega-menu-nav.liquidliquid
{% 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".

config.jsonjson
{ "name": "Brand Logos", "version": "1.0.0", "fields": {
  "brands": { "type": "api", "apiType": "brands", "label": "Brands" }
}}
PropertyTypeDefaultDescription
namestring—Brand name
slugstring | null—URL slug
imagestring | null—Brand logo URL
isActiveboolean—Active status
ordernumber—Display order
brand-logos.liquidliquid
<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.

config.jsonjson
{ "name": "Promo Banners", "version": "1.0.0", "fields": {
  "promotions": { "type": "promotion", "label": "Promotions" }
}}
PropertyTypeDefaultDescription
titlestring—Offer title
slugstring—URL slug
descriptionstring | null—Offer description
coverImagestring | null—Banner image URL
videostring | null—Video URL
productsarray—Product ID array
ordernumber—Display order
isActiveboolean—Active status
promo-banners.liquidliquid
{% 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.

config.jsonjson
{ "name": "Promo Detail", "version": "1.0.0", "fields": {
  "promotion": { "type": "promotional-products", "label": "Promotion" }
}}
promo-detail.liquidliquid
{% 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.

config.jsonjson
{ "name": "Featured Sections", "version": "1.0.0", "fields": {
  "featuredProducts": { "type": "featured-products", "label": "Featured Sections" }
}}
PropertyTypeDefaultDescription
titlestring—Section title
slugstring | null—URL slug
modestring—"manual" or "automatic"
automaticTypestring | null—"top_selling", "discounted", "most_popular", "category", "brand"
limitnumber20Max products
productsarray—Product objects
image1, image2, image3string | null—Section banner images
ordernumber—Display order
featured-sections.liquidliquid
{% 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 &rarr;</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.

config.jsonjson
{ "name": "Featured Detail", "version": "1.0.0", "fields": {
  "featured": { "type": "featured-section", "label": "Featured Section" }
}}
featured-detail.liquidliquid
{% 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".

config.jsonjson
{ "name": "FAQ Section", "version": "1.0.0", "fields": {
  "title": { "type": "text", "label": "Title", "default": "Frequently Asked Questions" },
  "faqs": { "type": "api", "apiType": "faqs", "label": "FAQs" }
}}
PropertyTypeDefaultDescription
questionstring—FAQ question
answerstring—FAQ answer (may contain HTML)
ordernumber—Display order
faq-accordion.liquidliquid
<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.

config.jsonjson
{ "name": "Store Locator", "version": "1.0.0", "fields": {
  "branches": { "type": "api", "apiType": "branches", "label": "Branches" }
}}
store-locator.liquidliquid
<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 &rarr;</a>{% endif %}
    </div>
  {% endfor %}
</div>

blog (Single Article)

Single shop article. Config type: "blog". Supports "automatic" on /blogs/:slug pages or a specific slug.

config.jsonjson
{ "name": "Blog Article", "version": "1.0.0", "fields": {
  "article": { "type": "blog", "label": "Article" }
}}
PropertyTypeDefaultDescription
titlestring—Article title
shortDescstring—Brief excerpt
contentstring—Full content (HTML)
coverImagestring | null—Cover image URL
slugstring—URL slug
tagsstring[]—Tag strings
seoTitlestring | null—SEO title
seoDescriptionstring | null—SEO description
publishedAtstring—Publish date (ISO string)
blog-article.liquidliquid
{% 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".

config.jsonjson
{ "name": "Blog List", "version": "1.0.0", "fields": {
  "blogList": { "type": "blog-list", "label": "Blog Articles" }
}}
PropertyTypeDefaultDescription
articlesarray—Article objects (same as single blog)
totalnumber—Total articles count
totalPagesnumber—Total pages
pagenumber—Current page number
limitnumber50Items per page
searchstring | undefined—Current search query
blog-list.liquidliquid
<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.

config.jsonjson
{ "name": "Order Status", "version": "1.0.0", "fields": {
  "order": { "type": "api", "apiType": "order-details", "label": "Order" }
}}
PropertyTypeDefaultDescription
orderNumberstring—Unique order number
statusstring—"pending", "confirmed", "approved", "packaging", "shipping", "shipped", "delivering", "delivered", "canceled"
fullNamestring—Customer name
emailstring | null—Customer email
mobileNumberstring—Customer phone
citystring | null—Delivery city
addressstring | null—Delivery address
itemsarray—Line items: { productName, variantName, quantity, price, totalPrice, extraFields }
subtotalnumber—Subtotal
taxnumber0Tax
shippingCostnumber0Shipping cost
discountnumber0Discount
totalAmountnumber—Final total
paymentMethodstring—Payment method
paidStatusstring—"unpaid", "paid", "partial"
trackingUrlstring | null—Tracking URL
trackingNumberstring | null—Tracking number
promocodestring | null—Applied promo code
statusHistoryarray—Array of { status, changedAt }
notesstring | null—Order notes
createdAtstring—Order date (ISO)
order-status.liquidliquid
{% 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".

config.jsonjson
{ "name": "Reviews", "version": "1.0.0", "fields": {
  "productRatings": { "type": "api", "apiType": "product-ratings", "label": "Ratings" }
}}
PropertyTypeDefaultDescription
ratingnumber—Rating 1–5
namestring—Reviewer name
reviewstring | null—Review text
createdAtstring—Review date (ISO)
reviews.liquidliquid
{% 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 %}&#9733;{% else %}&#9734;{% 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".

config.jsonjson
{ "name": "Search Results", "version": "1.0.0", "fields": {
  "searchData": { "type": "searchData", "label": "Search Data" }
}}
PropertyTypeDefaultDescription
qstring | undefined—Search query
brandSlugstring | undefined—Brand filter slug
categorySlugstring | undefined—Category filter slug
subcategorySlugstring | undefined—Subcategory filter slug
sortBystring"price"Sort field
sortOrderstring"asc""asc" or "desc"
productsarray—Matching product objects
search-results.liquidliquid
<section class="max-w-6xl mx-auto py-8 px-4">
  {% if searchData.q %}<h1 class="text-2xl font-bold">Results for &ldquo;{{ searchData.q }}&rdquo;</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.

TypeWidgetLiquid Value
text / stringText inputstring
textareaMulti-line textstring
numberNumber inputnumber
booleanToggle switchtrue / false
colorColor pickerhex string ("#ff0000")
dateDate pickerdate string
timeTime pickertime string
urlURL inputURL string
optionDropdown selectselected value string
imageImage uploaderimage URL string
iconIcon pickericon path (auto-converted to full URL)
toggleToggle selectorstring value
tabsTab selectorstring value
alignmentAlignment picker"left", "center", "right"
text-alignText alignment"left", "center", "right", "justify"
font-styleFont style pickerstring
paddingPadding controlsCSS value string
border-radiusBorder radiusCSS value string
sizeSize selectorstring
codeCode editorraw HTML/CSS string
wysiwygRich text editorHTML string
array_objectRepeatable itemsarray or { items: [...] }

API Data Fields Summary

Config TypeModeReturns
productsAlways fetchesArray of product objects (max 100)
product"automatic" or slugSingle product or null
api { apiType: "categories" }Always fetchesArray of categories
categories-expandedAlways fetchesCategories + subcategories + products nested
api { apiType: "brands" }Always fetches{ brands: [...] } (note: nested!)
promotion"all" or IDArray of promotional offers
promotional-products"automatic" or slugOffer with populated products
featured-products"all" or IDArray of featured sections
featured-section"automatic" or slugSingle featured section or null
api { apiType: "faqs" }Always fetchesArray of FAQs
api { apiType: "branches" }Always fetchesArray of branches
blog"automatic" or slugSingle article or null
blog-listAlways fetches{ articles, total, totalPages, page, limit }
api { apiType: "order-details" }Page contextOrder object
api { apiType: "product-ratings" }Page context / fetchArray of ratings
searchDataPage 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.

config.jsonjson
{
  "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 }
    }
  }
}
flexible.liquidliquid
{% 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.).

config.jsonjson
{
  "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.
testimonials.liquidliquid
<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">&ldquo;{{ t.quote }}&rdquo;</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 %}&#9733;{% else %}&#9734;{% endif %}{% endfor %}</div>
    </div>
  {% endfor %}
</div>

Option / Tabs Fields

Dropdowns and tab selectors let users pick from predefined values.

config.jsonjson
{
  "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" }
      ]
    }
  }
}
option-usage.liquidliquid
{% 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:

FilterUsageDescription
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
icon-chain.liquidliquid
{%- 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:

LimitValueDescription
parseLimit1 MBMaximum template source size
renderLimit10 secondsMaximum render time before timeout
memoryLimit10 millionMaximum 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 use limit on {% for %} loops.

Liquid Syntax Reference

Variables & Filters

variables-and-filters.liquidliquid
{%- 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 year

Conditionals

conditionals.liquidliquid
{%- 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

loops.liquidliquid
{%- 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.

render-examples.liquidliquid
{%- 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

assign-capture.liquidliquid
{%- 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 %} => 2

Whitespace Control

Use {%- and -%} (with hyphens) to strip whitespace around Liquid tags. Useful for keeping clean HTML output.

whitespace.liquidliquid
{%- 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 URLVariableDescription
/product/:slugproductFull product object with relations
/promotions/:slugpromotionPromotional offer with populated products
/featured/:slugfeaturedFeatured section with products
/order-status/:idorderOrder details with items and status history
/search?q=...searchDataSearch query, filters, and matching products
/blogs/:slugarticleBlog article with full HTML content
/blogsblogDataPaginated blog list with search/pagination
(review pages)productRatingsRating 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.

Mega Menu Header

A full mega menu with category navigation, search, cart, wishlist, sliding feature texts, mobile menu, and brand color integration. Uses categories-expanded for the navigation data and array_object for feature slides.

config.jsonjson
{
  "name": "Awesome Mega Menu",
  "version": "v1.0.5",
  "description": "Full mega menu header with search, cart, categories",
  "fields": {
    "topNotice": {
      "type": "string",
      "label": "Top Notice Bar Text",
      "value": "Welcome to our store",
      "placeholder": "Enter announcement text (leave empty to hide)"
    },
    "categoryMenu": {
      "type": "categories-expanded",
      "label": "Category Navigation",
      "value": []
    },
    "slidingTexts": {
      "type": "array_object",
      "label": "Feature Slides",
      "fields": {
        "text": { "type": "string", "label": "Title", "placeholder": "Enter the title" },
        "icon": { "type": "icon", "label": "Icon", "placeholder": "Select an icon" }
      },
      "value": [
        { "text": "Genuine Products", "icon": "/assets/icons/iconsax/bold/24-support.svg" },
        { "text": "Free Delivery", "icon": "/assets/icons/iconsax/bold/24-support.svg" },
        { "text": "24/7 Live Chat", "icon": "/assets/icons/iconsax/bold/24-support.svg" }
      ]
    }
  }
}

Key patterns in this example

  • CSS variables from shop.brandColor via a hidden div + inline script
  • Icon masking with CSS mask-image for colorable SVG icons
  • Subcategory fallback: checks cat.subcategories first, falls back to cat.products
  • Vanilla JS for interactivity (search, mega menu hover, mobile overlays) — no React needed
  • Feature slider using CSS transform: translateX with a setInterval
  • Badge updates via custom events (cart:updated, wishlist:updated)
mega-menu-header.liquid (abbreviated)liquid
<style>
  .mega-cat-btn.active-cat {
    background: var(--brand-color, #2563eb) !important;
    color: var(--brand-text-color, #fff) !important;
  }
</style>

<!-- Inject brand color as CSS variable -->
<div style="display:none;" id="headerBrandVars"
  data-brand-color="{{ shop.brandColor | default: '#2563eb' }}"
  data-brand-text="{{ shop.textOverBrandColor | default: '#ffffff' }}">
</div>
<script>
(function(){
  var v = document.getElementById('headerBrandVars');
  if (!v) return;
  document.documentElement.style.setProperty('--brand-color', v.dataset.brandColor);
  document.documentElement.style.setProperty('--brand-text-color', v.dataset.brandText);
})();
</script>

{% if topNotice != blank %}
<div class="text-xs font-medium py-1.5 text-center"
     style="background:var(--brand-color);color:var(--brand-text-color);">
  {{ topNotice }}
</div>
{% endif %}

<nav class="fixed top-0 left-0 right-0 z-50 bg-white border-b">
  <div class="max-w-7xl mx-auto flex items-center justify-between px-4">
    <!-- Logo -->
    <a href="/">
      {% if shop.shopLogo %}
        <img src="{{ shop.shopLogo }}" alt="{{ shop.shopName }}" class="py-4 max-h-[84px] object-contain">
      {% endif %}
    </a>

    <!-- Feature slides -->
    {% if slidingTexts.size > 0 %}
    <div class="hidden lg:block w-[250px] overflow-hidden rounded-lg py-3"
         style="background:color-mix(in srgb,var(--brand-color) 5%,transparent);">
      <div id="featureSlider" class="flex transition-transform duration-500">
        {% for slide in slidingTexts %}
        <div class="w-full shrink-0 flex items-center justify-center gap-2 text-sm">
          {% if slide.icon %}
            <span class="w-6 h-6 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;"></span>
          {% endif %}
          <span>{{ slide.text }}</span>
        </div>
        {% endfor %}
      </div>
    </div>
    {% endif %}

    <!-- Cart button -->
    <button onclick="window.openCart && window.openCart()"
            class="px-4 py-2 rounded-full text-sm font-semibold"
            style="background:var(--brand-color);color:var(--brand-text-color);">
      Cart <span id="cartBadge">0</span>
    </button>
  </div>

  <!-- Category bar with mega menu -->
  <div class="border-t">
    <div class="max-w-7xl mx-auto px-4">
      <div class="flex items-center gap-6 h-11 text-[13px]">
        <a href="/">Home</a>
        {% for cat in categoryMenu %}
          <a href="/categories/{{ cat.slug }}"
             onmouseenter="handleCategoryHover('{{ cat.slug }}')">{{ cat.name }}</a>
        {% endfor %}
      </div>

      <!-- Mega menu dropdown (hidden by default) -->
      <div id="megaMenuDropdown" style="display:none;" class="absolute bg-white shadow-2xl rounded-xl">
        {% for cat in categoryMenu %}
          <div class="mega-subcontent" data-cat="{{ cat.slug }}" style="display:none;">
            {% if cat.subcategories.size > 0 %}
              {% for sub in cat.subcategories %}
                {% if sub.products.size > 0 %}
                  <h3>{{ sub.name }}</h3>
                  {% for product in sub.products limit:5 %}
                    <a href="/products/{{ product.slug }}">
                      <img src="{{ product.coverImage }}" alt="{{ product.name }}" loading="lazy">
                      <p>{{ product.name }}</p>
                    </a>
                  {% endfor %}
                {% endif %}
              {% endfor %}
            {% elsif cat.products.size > 0 %}
              {% for product in cat.products limit:5 %}
                <a href="/products/{{ product.slug }}">
                  <img src="{{ product.coverImage }}" alt="{{ product.name }}" loading="lazy">
                  <p>{{ product.name }}</p>
                </a>
              {% endfor %}
            {% else %}
              <p>No products found</p>
            {% endif %}
          </div>
        {% endfor %}
      </div>
    </div>
  </div>
</nav>

Footer Component

config.jsonjson
{
  "name": "Store Footer",
  "version": "1.0.0",
  "fields": {
    "copyrightText": { "type": "text", "label": "Copyright Text", "default": "All rights reserved." },
    "showNewsletter": { "type": "boolean", "label": "Show Newsletter", "default": true }
  }
}
footer-1.liquidliquid
<footer class="bg-gray-900 text-white py-12 px-4">
  <div class="max-w-6xl mx-auto grid md:grid-cols-4 gap-8">
    <!-- Brand -->
    <div>
      {% if shop.footerLogo %}<img src="{{ shop.footerLogo }}" alt="{{ shop.shopName }}" class="h-10 mb-4">
      {% else %}<h3 class="text-lg font-bold mb-4">{{ shop.shopName }}</h3>{% endif %}
      <p class="text-sm text-gray-400">{{ shop.shopDescription | truncate: 120 }}</p>
    </div>

    <!-- Quick Links -->
    {% if shop.footerQuickLinks.size > 0 %}
    <div>
      <h4 class="font-semibold mb-4">Quick Links</h4>
      {% for link in shop.footerQuickLinks %}
        <a href="{{ link.url }}" class="block text-sm text-gray-400 hover:text-white py-1">{{ link.title }}</a>
      {% endfor %}
    </div>
    {% endif %}

    <!-- Contact -->
    <div>
      <h4 class="font-semibold mb-4">Contact</h4>
      {% if shop.publicEmail != blank %}<p class="text-sm text-gray-400">{{ shop.publicEmail }}</p>{% endif %}
      {% if shop.publicContactNumber != blank %}<p class="text-sm text-gray-400">{{ shop.publicContactNumber }}</p>{% endif %}
      {% if shop.location != blank %}<p class="text-sm text-gray-400 mt-2">{{ shop.location }}</p>{% endif %}
    </div>

    <!-- Social + Newsletter -->
    <div>
      {% if shop.socialMediaLinks.size > 0 %}
        <h4 class="font-semibold mb-4">Follow Us</h4>
        <div class="flex gap-3">
          {% for link in shop.socialMediaLinks %}
            <a href="{{ link.url }}" target="_blank" class="w-9 h-9 rounded-full bg-gray-800 flex items-center justify-center text-xs hover:bg-gray-700">
              {{ link.title | slice: 0, 2 | upcase }}
            </a>
          {% endfor %}
        </div>
      {% endif %}
      {% if showNewsletter %}
        <div class="mt-6">
          <p class="text-sm text-gray-400 mb-2">Subscribe to our newsletter</p>
          <div class="flex"><input type="email" placeholder="Email" class="flex-1 px-3 py-2 rounded-l bg-gray-800 text-sm outline-none"><button class="px-4 py-2 rounded-r text-sm font-medium" style="background:{{ shop.brandColor }};color:{{ shop.textOverBrandColor }};">Subscribe</button></div>
        </div>
      {% endif %}
    </div>
  </div>

  <!-- Bottom bar -->
  <div class="max-w-6xl mx-auto border-t border-gray-800 mt-8 pt-6 flex flex-wrap items-center justify-between gap-4">
    <p class="text-xs text-gray-500">&copy; {{ "now" | date: "%Y" }} {{ shop.shopName }}. {{ copyrightText }}</p>
    <div class="flex gap-2">
      {% for method in shop.paymentMethods %}<span class="text-xs bg-gray-800 px-2 py-1 rounded text-gray-400">{{ method }}</span>{% endfor %}
    </div>
    {% if shop.panVatNumber %}<p class="text-xs text-gray-600">PAN/VAT: {{ shop.panVatNumber }}</p>{% endif %}
  </div>
</footer>

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.

config.jsonjson
{
  "name": "Product Card",
  "version": "1.0.0",
  "description": "Reusable product card block",
  "fields": {}
}
product-card.liquidliquid
<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 %}&#9733;{% else %}&#9734;{% 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-element onclick handlers for large lists
  • Use conditional fetching (condition in 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:hidden to show/hide elements by breakpoint (e.g. mobile menu vs desktop nav)
  • Images should use class="w-full h-auto" or aspect-square object-cover for consistent sizing
  • Keep touch targets at least 44x44px on mobile (buttons, links)

SEO

  • Always include alt attributes 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" and target="_blank" on external links
  • Use line-clamp-2 for 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

The commerce operating system for businesses starting in Nepal and growing beyond it.

Practical commerce notes

PlatformStorefrontAndroid appSales channelsPaymentsOrder automationAI
BuildThemesPluginsDevelopersDomains
CompanyPricingBlogsPrivacyTerms
ContactWhatsApp[email protected]Bhaktapur, Nepal
© 2026 Zalient NepalAll systems operational