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 informationshop.name- Store nameshop.description- Store descriptionshop.domain- Store domainshop.email- Store email
-
tenant- Tenant configurationtenant.id- Tenant identifiertenant.theme- Current theme nametenant.api_url- API base URL
-
store- Store details from APIstore.id- Store IDstore.name- Store namestore.settings- Store settings object
Page-Specific Objects
Product Page (product.liquid)
product- Current productproduct.id- Product IDproduct.name- Product nameproduct.description- Product descriptionproduct.price- Product priceproduct.compare_at_price- Compare at priceproduct.images- Product images arrayproduct.variants- Product variantsproduct.available- Availability statusproduct.tags- Product tags
Collection Page (collection.liquid)
-
collection- Current collectioncollection.id- Collection IDcollection.name- Collection namecollection.description- Collection descriptioncollection.products- Products in collection
-
products- Product listings- Array of product objects
Cart Page (cart.liquid)
cart- Shopping cartcart.items- Cart items arraycart.total_price- Total pricecart.item_count- Number of itemscart.subtotal- Subtotalcart.total- Final total
Search Page (search.liquid)
search- Search resultssearch.results- Search results arraysearch.terms- Search querysearch.results_count- Number of results
Page Object (page.liquid)
page- Current pagepage.id- Page IDpage.title- Page titlepage.content- Page content (HTML)page.url- Page URL
Widget Objects
widgets- Dynamic widgets organized by sectionwidgets.hero- Hero section widgetswidgets.content- Content section widgetswidgets.footer- Footer widgetswidgets.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
- Layout (
layout/theme.liquid) - Base template wrapping all pages - Template (
templates/*.liquid) - Specific page templates that use{% layout 'theme' %} - Sections (
sections/*.liquid) - Reusable page sections included via{% section 'name' %} - 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:
- Check if object exists:
{% if product %}
{{ product.name }}
{% else %}
<p>Product not found</p>
{% endif %}
- Use default filter:
{{ product.name | default: 'Unknown Product' }}
- 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:
- 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 %}
- Check array structure:
Items count: {{ items.size }}
First item: {{ items[0] | json }}
Issue: Filter Not Working
Problem: {{ price | money }} shows wrong format
Solutions:
- Check filter syntax:
{{ price | money }} ✅ Correct
{{ price | money() }} ❌ Wrong (no parentheses)
- Verify filter exists - see Filters documentation
Issue: Section Not Rendering
Problem: {% section 'header' %} doesn't show
Solutions:
- Verify section file exists:
sections/header.liquid - Check for syntax errors in section file
- Ensure section has valid Liquid syntax
Debugging Tools
- Use
jsonfilter to inspect objects:
<pre>{{ product | json }}</pre>
- Add debug comments:
{% comment %}
Debug info:
- Product ID: {{ product.id }}
- Product Name: {{ product.name }}
- Available: {{ product.available }}
{% endcomment %}
- Check browser console for JavaScript errors
- Use Liquid Playground to test code snippets
Best Practices
Template Organization
- Use Layouts: Always use the layout system for consistent page structure
- Reusable Snippets: Create snippets for repeated components
- Section Organization: Organize sections logically
- Asset Management: Use
asset_urlfilter for all assets - Error Handling: Use
defaultfilter for optional values - Performance: Minimize nested loops and complex logic
- Accessibility: Use semantic HTML and proper ARIA labels
Code Quality
- Consistent Indentation: Use 2 spaces for indentation
- Meaningful Comments: Document complex logic
- DRY Principle: Don't repeat yourself - use snippets
- Semantic HTML: Use proper HTML5 elements
- Security: Escape user input with
escapefilter
Performance
- Limit Loops: Use
limitfilter for large collections - Cache Values: Use
assignfor computed values - Lazy Loading: Use
loading="lazy"for images - Minimize Logic: Keep template logic simple
- Optimize Images: Use appropriate image sizes
Liquid vs Other Templating Engines
| Feature | Liquid | Handlebars | Mustache | EJS |
|---|---|---|---|---|
| Syntax | {{ }} and {% %} | {{ }} and {{# }} | {{ }} | <% %> |
| Logic | Built-in tags | Helpers required | Logic-less | Full JavaScript |
| Filters | Built-in filters | Helpers | No filters | JavaScript functions |
| Inheritance | Layout system | Partials | Partials | Includes |
| Learning Curve | Moderate | Easy | Very Easy | Easy (if you know JS) |
| Performance | Fast | Fast | Very Fast | Moderate |
| Ecosystem | Shopify/O2VEND | Large | Large | Node.js focused |
| Best For | E-commerce themes | Web apps | Simple templates | Node.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>