Skip to main content

Liquid Templates

O2VEND uses Shopify-compatible Liquid templates for theme development. This guide covers the template structure, syntax, and available objects.

Template Structure

Themes are organized in the following structure:

themes/
theme-name/
layout/
theme.liquid # Main layout template
templates/
index.liquid # Home page
product.liquid # Product detail page
collection.liquid # Collection/category page
list-collections.liquid # Collections listing
page.liquid # Custom pages
cart.liquid # Shopping cart
search.liquid # Search results
categories.liquid # Categories listing
products.liquid # Products listing
checkout.liquid # Checkout page
order-confirmation.liquid # Order confirmation
login.liquid # Login page
address-book.liquid # Address book
templates/
account/ # Account pages
dashboard.liquid
orders.liquid
order-detail.liquid
profile.liquid
wishlist.liquid
loyalty.liquid
sections/ # Reusable sections
header.liquid
footer.liquid
hero.liquid
content.liquid
snippets/ # Reusable components
product-card.liquid
breadcrumbs.liquid
pagination.liquid
assets/ # CSS, JS, images
theme.css
theme.js
components.css
config/ # Theme configuration
settings_schema.json
settings_data.json
locales/ # Translations
en.default.json

Layout System

Main Layout

The layout/theme.liquid file is the base template that wraps all pages:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title }}</title>

<!-- Theme CSS -->
{{ 'theme.css' | asset_url | stylesheet_tag }}

<!-- App hooks -->
{% render 'hook', hook_name: 'theme_head' %}
</head>
<body>
{% render 'hook', hook_name: 'theme_body_begin' %}

<!-- Header -->
{% section 'header' %}

<!-- Main Content -->
<main>
{{ content }}
</main>

<!-- Footer -->
{% section 'footer' %}

<!-- Theme JavaScript -->
{{ 'theme.js' | asset_url | script_tag }}

<!-- App hooks -->
{% render 'hook', hook_name: 'theme_body_end' %}
</body>
</html>

Template Layout Directive

Templates can specify a layout using the layout directive:

{% layout 'theme' %}

<div class="page-content">
<h1>{{ page.title }}</h1>
{{ page.content }}
</div>

Available Objects

📚 Complete Reference: For a comprehensive list of all available objects, properties, and data structures, see the Liquid Objects Reference.

Global Objects

  • shop - Store information

    • shop.name - Store name
    • shop.description - Store description
    • shop.domain - Store domain
    • shop.email - Store email
  • tenant - Tenant configuration

    • tenant.id - Tenant identifier
    • tenant.theme - Current theme name
    • tenant.api_url - API base URL
  • store - Store details from API

    • store.id - Store ID
    • store.name - Store name
    • store.settings - Store settings object

Page-Specific Objects

Product Page (product.liquid)

  • product - Current product
    • product.id - Product ID
    • product.name - Product name
    • product.description - Product description
    • product.price - Product price
    • product.compare_at_price - Compare at price
    • product.images - Product images array
    • product.variants - Product variants
    • product.available - Availability status
    • product.tags - Product tags

Collection Page (collection.liquid)

  • collection - Current collection

    • collection.id - Collection ID
    • collection.name - Collection name
    • collection.description - Collection description
    • collection.products - Products in collection
  • products - Product listings

    • Array of product objects

Cart Page (cart.liquid)

  • cart - Shopping cart
    • cart.items - Cart items array
    • cart.total_price - Total price
    • cart.item_count - Number of items
    • cart.subtotal - Subtotal
    • cart.total - Final total

Search Page (search.liquid)

  • search - Search results
    • search.results - Search results array
    • search.terms - Search query
    • search.results_count - Number of results

Page Object (page.liquid)

  • page - Current page
    • page.id - Page ID
    • page.title - Page title
    • page.content - Page content (HTML)
    • page.url - Page URL

Widget Objects

  • widgets - Dynamic widgets organized by section
    • widgets.hero - Hero section widgets
    • widgets.content - Content section widgets
    • widgets.footer - Footer widgets
    • widgets.sidebar - Sidebar widgets

Example:

{% for widget in widgets.hero %}
{% render 'widget', widget: widget %}
{% endfor %}

Liquid Syntax

Variables

{{ variable_name }}
{{ object.property }}
{{ array[0] }}

Filters

Filters transform output:

{{ product.name | upcase }}
{{ product.price | money }}
{{ product.description | truncate: 100 }}

See the Filters documentation for a complete list.

Tags

Control Flow

{% if condition %}
Content
{% elsif other_condition %}
Other content
{% else %}
Default content
{% endif %}

{% unless condition %}
Content when condition is false
{% endunless %}

{% case variable %}
{% when 'value1' %}
Content 1
{% when 'value2' %}
Content 2
{% else %}
Default content
{% endcase %}

Loops

{% for item in items %}
{{ item.name }}
{% endfor %}

{% for i in (1..10) %}
{{ i }}
{% endfor %}

Includes and Sections

{% render 'snippet-name', variable: value %}
{% section 'section-name' %}
{% include 'snippet-name' %}

Comments

{% comment %}
This is a comment
It won't appear in the output
{% endcomment %}

Template Inheritance

O2VEND uses a hierarchical template system where templates inherit from layouts:

graph TB
Layout[layout/theme.liquid<br/>Base Layout] --> Template1[templates/index.liquid<br/>Home Page]
Layout --> Template2[templates/product.liquid<br/>Product Page]
Layout --> Template3[templates/collection.liquid<br/>Collection Page]
Template1 --> Section1[sections/hero.liquid]
Template2 --> Section2[sections/product-details.liquid]
Section1 --> Snippet1[snippets/product-card.liquid]
Section2 --> Snippet2[snippets/product-form.liquid]

How Template Inheritance Works

  1. Layout (layout/theme.liquid) - Base template wrapping all pages
  2. Template (templates/*.liquid) - Specific page templates that use {% layout 'theme' %}
  3. Sections (sections/*.liquid) - Reusable page sections included via {% section 'name' %}
  4. Snippets (snippets/*.liquid) - Reusable components included via {% render 'name' %}

The {{ content }} variable in the layout is replaced by the template content.

Template Examples

Complete Product Page Template

{% layout 'theme' %}

<div class="product-page">
<div class="product-gallery">
{% if product.images.size > 0 %}
<div class="main-image">
<img src="{{ product.images[0] | img_url: 'large' }}"
alt="{{ product.name | escape }}"
id="main-product-image">
</div>
{% if product.images.size > 1 %}
<div class="thumbnail-images">
{% for image in product.images %}
<img src="{{ image | img_url: 'medium' }}"
alt="{{ product.name | escape }}"
onclick="changeMainImage('{{ image | img_url: 'large' }}')"
class="thumbnail">
{% endfor %}
</div>
{% endif %}
{% else %}
<div class="no-image">
<p>No image available</p>
</div>
{% endif %}
</div>

<div class="product-info">
<h1 class="product-title">{{ product.name }}</h1>

<div class="product-meta">
{% if product.vendor %}
<p class="vendor">By {{ product.vendor }}</p>
{% endif %}
{% if product.tags.size > 0 %}
<div class="tags">
{% for tag in product.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="product-price">
<span class="current-price">{{ product.price | money }}</span>
{% if product.compare_at_price > product.price %}
<span class="compare-price">{{ product.compare_at_price | money }}</span>
<span class="discount">
Save {{ product.compare_at_price | minus: product.price | money }}
</span>
{% endif %}
</div>

<div class="product-description">
{{ product.description }}
</div>

{% if product.available %}
<form action="/cart/add" method="post" class="product-form">
{% if product.variants.size > 1 %}
<div class="variant-selector">
<label for="variant-id">Select Variant:</label>
<select name="id" id="variant-id" required>
{% for variant in product.variants %}
<option value="{{ variant.id }}"
{% unless variant.available %}disabled{% endunless %}
data-price="{{ variant.price | money }}">
{{ variant.title }} - {{ variant.price | money }}
{% unless variant.available %} (Sold Out){% endunless %}
</option>
{% endfor %}
</select>
</div>
{% else %}
<input type="hidden" name="id" value="{{ product.variants[0].id }}">
{% endif %}

<div class="quantity-selector">
<label for="quantity">Quantity:</label>
<input type="number"
name="quantity"
id="quantity"
value="1"
min="1"
max="10"
required>
</div>

<button type="submit" class="add-to-cart-btn">
Add to Cart
</button>
</form>
{% else %}
<p class="out-of-stock">This product is currently unavailable</p>
{% endif %}

{% section 'product-recommendations' %}
</div>
</div>

Shopping Cart Template

{% layout 'theme' %}

<div class="cart-page">
<h1>Shopping Cart</h1>

{% if cart.items.size > 0 %}
<form action="/cart/update" method="post" class="cart-form">
<table class="cart-table">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
{% for item in cart.items %}
<tr class="cart-item" data-item-id="{{ item.id }}">
<td class="item-product">
{% if item.image %}
<img src="{{ item.image | img_url: 'small' }}"
alt="{{ item.title | escape }}">
{% endif %}
<div class="item-details">
<h3>{{ item.title }}</h3>
{% if item.variant_title != 'Default Title' %}
<p class="variant-title">{{ item.variant_title }}</p>
{% endif %}
</div>
</td>
<td class="item-price">
{{ item.price | money }}
</td>
<td class="item-quantity">
<input type="number"
name="updates[{{ item.id }}]"
value="{{ item.quantity }}"
min="0"
class="quantity-input">
</td>
<td class="item-total">
{{ item.line_price | money }}
</td>
<td class="item-remove">
<button type="button"
class="remove-item"
data-item-id="{{ item.id }}">
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>

<div class="cart-actions">
<button type="submit" class="update-cart-btn">Update Cart</button>
<a href="/collections/all" class="continue-shopping">Continue Shopping</a>
</div>
</form>

<div class="cart-summary">
<div class="cart-totals">
<div class="subtotal">
<span>Subtotal:</span>
<span>{{ cart.subtotal | money }}</span>
</div>
{% if cart.total_discount > 0 %}
<div class="discount">
<span>Discount:</span>
<span>-{{ cart.total_discount | money }}</span>
</div>
{% endif %}
<div class="total">
<span>Total:</span>
<span>{{ cart.total | money }}</span>
</div>
</div>

<a href="/checkout" class="checkout-btn">Proceed to Checkout</a>
</div>
{% else %}
<div class="empty-cart">
<p>Your cart is empty</p>
<a href="/collections/all" class="shop-now-btn">Start Shopping</a>
</div>
{% endif %}
</div>

Checkout Page Template

{% layout 'theme' %}

<div class="checkout-page">
<h1>Checkout</h1>

<form action="/checkout/complete" method="post" class="checkout-form">
<div class="checkout-sections">
<!-- Shipping Address -->
<section class="checkout-section">
<h2>Shipping Address</h2>
<div class="form-group">
<label for="shipping-name">Full Name *</label>
<input type="text"
id="shipping-name"
name="shipping[name]"
required>
</div>
<div class="form-group">
<label for="shipping-address">Address *</label>
<input type="text"
id="shipping-address"
name="shipping[address1]"
required>
</div>
<div class="form-row">
<div class="form-group">
<label for="shipping-city">City *</label>
<input type="text"
id="shipping-city"
name="shipping[city]"
required>
</div>
<div class="form-group">
<label for="shipping-zip">ZIP Code *</label>
<input type="text"
id="shipping-zip"
name="shipping[zip]"
required>
</div>
</div>
</section>

<!-- Payment Method -->
<section class="checkout-section">
<h2>Payment Method</h2>
<div class="payment-methods">
{% for method in payment_methods %}
<label class="payment-method">
<input type="radio"
name="payment_method"
value="{{ method.id }}"
{% if forloop.first %}checked{% endif %}>
<span>{{ method.name }}</span>
</label>
{% endfor %}
</div>
</section>

<!-- Order Summary -->
<section class="checkout-section order-summary">
<h2>Order Summary</h2>
<div class="order-items">
{% for item in cart.items %}
<div class="order-item">
<span class="item-name">{{ item.title }}</span>
<span class="item-quantity">x{{ item.quantity }}</span>
<span class="item-price">{{ item.line_price | money }}</span>
</div>
{% endfor %}
</div>
<div class="order-totals">
<div class="total-line">
<span>Subtotal:</span>
<span>{{ cart.subtotal | money }}</span>
</div>
<div class="total-line">
<span>Shipping:</span>
<span>{{ shipping_cost | money | default: 'Calculated at next step' }}</span>
</div>
<div class="total-line total">
<span>Total:</span>
<span>{{ cart.total | money }}</span>
</div>
</div>
</section>
</div>

<button type="submit" class="complete-order-btn">Complete Order</button>
</form>
</div>

Performance Tips for Template Rendering

1. Minimize Nested Loops

Avoid deep nesting:

{% for collection in collections %}
{% for product in collection.products %}
{% for variant in product.variants %}
{% for option in variant.options %}
{{ option.name }}
{% endfor %}
{% endfor %}
{% endfor %}
{% endfor %}

Use snippets to break down complexity:

{% for collection in collections %}
{% render 'collection-grid', collection: collection %}
{% endfor %}

2. Cache Expensive Operations

Use Liquid's assign to cache computed values:

{% assign product_count = collection.products.size %}
{% assign has_discount = product.compare_at_price > product.price %}

3. Limit Loop Iterations

Use limit to restrict loop iterations:

{% for product in collection.products limit: 12 %}
{% render 'product-card', product: product %}
{% endfor %}

4. Lazy Load Images

Use responsive image sizes:

<img src="{{ image | img_url: 'small' }}" 
srcset="{{ image | img_url: 'medium' }} 2x"
loading="lazy"
alt="{{ product.name }}">

5. Defer JavaScript Loading

Load non-critical JavaScript at the end:

{{ 'theme.js' | asset_url | script_tag }}
<script defer src="{{ 'analytics.js' | asset_url }}"></script>

Debugging Template Issues

Common Issues and Solutions

Issue: Variable Not Rendering

Problem: {{ product.name }} shows nothing

Solutions:

  1. Check if object exists:
{% if product %}
{{ product.name }}
{% else %}
<p>Product not found</p>
{% endif %}
  1. Use default filter:
{{ product.name | default: 'Unknown Product' }}
  1. Check object structure:
{% comment %} Debug: Check what's available {% endcomment %}
<pre>{{ product | json }}</pre>

Issue: Loop Not Executing

Problem: {% for item in items %} doesn't render

Solutions:

  1. Verify array exists and has items:
{% if items and items.size > 0 %}
{% for item in items %}
{{ item.name }}
{% endfor %}
{% else %}
<p>No items found</p>
{% endif %}
  1. Check array structure:
Items count: {{ items.size }}
First item: {{ items[0] | json }}

Issue: Filter Not Working

Problem: {{ price | money }} shows wrong format

Solutions:

  1. Check filter syntax:
{{ price | money }}  ✅ Correct
{{ price | money() }} ❌ Wrong (no parentheses)
  1. Verify filter exists - see Filters documentation

Issue: Section Not Rendering

Problem: {% section 'header' %} doesn't show

Solutions:

  1. Verify section file exists: sections/header.liquid
  2. Check for syntax errors in section file
  3. Ensure section has valid Liquid syntax

Debugging Tools

  1. Use json filter to inspect objects:
<pre>{{ product | json }}</pre>
  1. Add debug comments:
{% comment %}
Debug info:
- Product ID: {{ product.id }}
- Product Name: {{ product.name }}
- Available: {{ product.available }}
{% endcomment %}
  1. Check browser console for JavaScript errors
  2. Use Liquid Playground to test code snippets

Best Practices

Template Organization

  1. Use Layouts: Always use the layout system for consistent page structure
  2. Reusable Snippets: Create snippets for repeated components
  3. Section Organization: Organize sections logically
  4. Asset Management: Use asset_url filter for all assets
  5. Error Handling: Use default filter for optional values
  6. Performance: Minimize nested loops and complex logic
  7. Accessibility: Use semantic HTML and proper ARIA labels

Code Quality

  1. Consistent Indentation: Use 2 spaces for indentation
  2. Meaningful Comments: Document complex logic
  3. DRY Principle: Don't repeat yourself - use snippets
  4. Semantic HTML: Use proper HTML5 elements
  5. Security: Escape user input with escape filter

Performance

  1. Limit Loops: Use limit filter for large collections
  2. Cache Values: Use assign for computed values
  3. Lazy Loading: Use loading="lazy" for images
  4. Minimize Logic: Keep template logic simple
  5. Optimize Images: Use appropriate image sizes

Liquid vs Other Templating Engines

FeatureLiquidHandlebarsMustacheEJS
Syntax{{ }} and {% %}{{ }} and {{# }}{{ }}<% %>
LogicBuilt-in tagsHelpers requiredLogic-lessFull JavaScript
FiltersBuilt-in filtersHelpersNo filtersJavaScript functions
InheritanceLayout systemPartialsPartialsIncludes
Learning CurveModerateEasyVery EasyEasy (if you know JS)
PerformanceFastFastVery FastModerate
EcosystemShopify/O2VENDLargeLargeNode.js focused
Best ForE-commerce themesWeb appsSimple templatesNode.js apps

Why Liquid for O2VEND?

  • Shopify-compatible syntax (familiar to many developers)
  • Built-in e-commerce filters (money, product_url, etc.)
  • Safe by default (prevents XSS)
  • Excellent for theme development
  • Strong O2VEND ecosystem support

Example Template

{% layout 'theme' %}

<div class="product-page">
<div class="product-images">
{% for image in product.images %}
<img src="{{ image | img_url: 'large' }}" alt="{{ product.name }}">
{% endfor %}
</div>

<div class="product-details">
<h1>{{ product.name }}</h1>

<div class="product-price">
<span class="price">{{ product.price | money }}</span>
{% if product.compare_at_price > product.price %}
<span class="compare-price">{{ product.compare_at_price | money }}</span>
{% endif %}
</div>

<div class="product-description">
{{ product.description }}
</div>

<form action="/cart/add" method="post">
<select name="id">
{% for variant in product.variants %}
<option value="{{ variant.id }}">{{ variant.title }}</option>
{% endfor %}
</select>

<input type="number" name="quantity" value="1" min="1">
<button type="submit">Add to Cart</button>
</form>
</div>
</div>

Next Steps