Webshop
Nimbu provides a complete e-commerce platform with products, variants, collections, shopping cart, checkout, and order management. Build custom storefronts with full control over the shopping experience.
Products
Products are accessible through the products drop and individual product pages via the product variable.
Product Properties
| Property | Description |
|---|---|
id | Unique product ID |
name | Product name |
description | HTML description |
sku | Stock keeping unit |
price | Base price (cents) |
on_sale | Boolean: product on sale |
on_sale_price | Sale price if on sale |
effective_price | Current price (sale or regular) |
price_for_current_customer | Customer-specific pricing |
url | Product page URL |
status | "active", "sold_out", "hidden" |
current_stock | Available inventory |
amount_in_cart | Quantity in customer's cart |
images | Array of product images |
variants | Array of product variants |
vendor | Brand/manufacturer name |
type | Product type/category |
weight | Weight in grams |
tax_scheme | Tax configuration |
seo_title, seo_description | SEO metadata |
| Custom attributes | Any custom fields defined |
Product Template
templates/product.liquid:
<div class="product-page">
<div class="container">
{% breadcrumbs %}
<div class="product-main">
<!-- Product Images -->
<div class="product-images">
{% if product.images.size > 0 %}
<div class="main-image">
<img src="{{ product.images.first.url | filter, width: '800px' }}"
alt="{{ product.name }}"
id="main-product-image">
</div>
{% if product.images.size > 1 %}
<div class="image-thumbnails">
{% for image in product.images %}
<img src="{{ image.url | filter, width: '100px' }}"
alt="{{ product.name }}"
data-full="{{ image.url | filter, width: '800px' }}"
onclick="document.getElementById('main-product-image').src = this.dataset.full">
{% endfor %}
</div>
{% endif %}
{% endif %}
</div>
<!-- Product Info -->
<div class="product-info">
<h1>{{ product.name }}</h1>
{% if product.vendor %}
<p class="vendor">{{ product.vendor }}</p>
{% endif %}
<div class="product-price">
{% if product.on_sale %}
<span class="sale-price">{{ product.on_sale_price | money_with_currency }}</span>
<span class="original-price">{{ product.price | money_with_currency }}</span>
<span class="save-badge">
Save {{ product.price | minus: product.on_sale_price | money_with_currency }}
</span>
{% else %}
<span class="price">{{ product.price | money_with_currency }}</span>
{% endif %}
</div>
<div class="product-description wysiwyg">
{{ product.description }}
</div>
<!-- Variants & Add to Cart -->
{% if product.status != 'sold_out' %}
{{ product | add_to_cart,
btn_class: 'btn btn-primary btn-lg',
add_label: 'Add to Cart',
show_quantity_buttons: true
}}
{% else %}
<p class="sold-out-message">This product is currently sold out.</p>
{% endif %}
<!-- Product Details -->
<div class="product-details">
<h3>Details</h3>
<dl>
{% if product.sku %}
<dt>SKU</dt>
<dd>{{ product.sku }}</dd>
{% endif %}
{% if product.type %}
<dt>Type</dt>
<dd>{{ product.type }}</dd>
{% endif %}
<dt>Availability</dt>
<dd>
{% if product.current_stock > 0 %}
In stock ({{ product.current_stock }} available)
{% else %}
Out of stock
{% endif %}
</dd>
</dl>
</div>
</div>
</div>
<!-- Related Products -->
<div class="related-products">
<h2>You May Also Like</h2>
<div class="product-grid">
{% scope product_type_name == product.product_type_name %}
{% for related in products.random limit: 4 %}
{% unless related.id == product.id %}
{% include 'product-card', product: related %}
{% endunless %}
{% endfor %}
{% endscope %}
</div>
</div>
</div>
</div>Working with Variants
Products can have variants (sizes, colors, etc.):
<!-- Variants with add_to_cart helper -->
{{ product | add_to_cart,
btn_class: 'btn-primary',
hide_variants: false,
show_quantity_dropdown: true
}}
<!-- Manual variant selection -->
<select name="variant_id" class="variant-selector">
{% for variant in product.variants %}
<option value="{{ variant.id }}"
{% unless variant.stock > 0 %}disabled{% endunless %}>
{{ variant.name }}
{% if variant.stock > 0 %}
- {{ variant.price | money_with_currency }}
{% else %}
- Sold Out
{% endif %}
</option>
{% endfor %}
</select>Variant Properties
| Property | Description |
|---|---|
id | Variant ID |
name | Variant name (e.g., "Size 42") |
price | Variant-specific price |
on_sale_price | Sale price if applicable |
sku | Variant SKU |
stock | Available inventory |
weight | Weight in grams |
url | Product URL with variant selected |
Collections
Collections group related products together.
Collection Template
templates/collection.liquid:
<div class="collection-page">
<div class="container">
<header class="collection-header">
<h1>{{ collection.name }}</h1>
{% if collection.description %}
<div class="collection-description">{{ collection.description }}</div>
{% endif %}
<p class="product-count">{{ collection.product_count }} products</p>
</header>
<!-- Filters -->
<div class="collection-filters">
<form action="{{ collection.url }}" method="get">
<!-- Filter by attribute -->
<select name="color">
<option value="">All Colors</option>
{% for color in products.attributes_in_use.color %}
<option value="{{ color }}" {% if params.color == color %}selected{% endif %}>
{{ color }}
</option>
{% endfor %}
</select>
<!-- Sort -->
<select name="sort">
<option value="">Default</option>
<option value="price_asc" {% if params.sort == 'price_asc' %}selected{% endif %}>
Price: Low to High
</option>
<option value="price_desc" {% if params.sort == 'price_desc' %}selected{% endif %}>
Price: High to Low
</option>
<option value="name_asc" {% if params.sort == 'name_asc' %}selected{% endif %}>
Name: A-Z
</option>
</select>
<button type="submit">Apply Filters</button>
</form>
</div>
<!-- Products Grid -->
<div class="products-grid">
{% paginate collection.products by 12 %}
{% for product in collection.products %}
{% include 'product-card', product: product %}
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}
</div>
</div>
</div>Collection Properties
| Property | Description |
|---|---|
name | Collection name |
description | HTML description |
slug | URL slug |
url | Collection page URL |
products | Products in collection |
product_count | Number of products |
current? | Boolean: viewing this collection |
Filtering Products
Use {% scope %} to filter products:
<!-- Filter by collection -->
{% scope collection_slugs contains 'summer-2024' %}
{% for product in products.all %}
{% include 'product-card', product: product %}
{% endfor %}
{% endscope %}
<!-- Multiple conditions -->
{% scope vendor == 'Nike' and price > 5000 and on_sale == true %}
{% for product in products.all %}
{% include 'product-card', product: product %}
{% endfor %}
{% endscope %}
<!-- Filter by custom attribute -->
{% scope color == params.color %}
{% for product in products.all %}
{% include 'product-card', product: product %}
{% endfor %}
{% endscope %}See Channels for complete scope documentation.
Shopping Cart
Access the cart through the cart drop (available when session exists).
Cart Properties
| Property | Description |
|---|---|
items | Array of line items |
items_count | Total items |
total_price | Total before shipping/tax |
total_weight | Total weight |
subtotal_price | Subtotal |
shipping_price | Shipping cost |
taxes | Tax amount |
grand_total | Final total with shipping/taxes |
Add to Cart
Use the add_to_cart filter:
<!-- Basic -->
{{ product | add_to_cart }}
<!-- With options -->
{{ product | add_to_cart,
btn_class: 'btn btn-primary',
add_label: 'Add to Cart',
default_quantity: 1,
hide_variants: false,
show_quantity_dropdown: true,
show_quantity_buttons: true
}}Cart Page
templates/cart.liquid:
<div class="cart-page">
<div class="container">
<h1>Shopping Cart</h1>
{% if cart.items.size > 0 %}
<div class="cart-items">
{% for item in cart.items %}
<div class="cart-item">
<div class="item-image">
<img src="{{ item.product.images.first.url | filter, width: '150px' }}"
alt="{{ item.product.name }}">
</div>
<div class="item-details">
<h3><a href="{{ item.product.url }}">{{ item.product.name }}</a></h3>
{% if item.variant %}
<p class="variant">{{ item.variant.name }}</p>
{% endif %}
<p class="price">{{ item.price | money_with_currency }}</p>
</div>
<div class="item-quantity">
{{ item | update_cart_item, show_quantity_buttons: true }}
</div>
<div class="item-total">
<p class="line-total">{{ item.line_price | money_with_currency }}</p>
</div>
<div class="item-remove">
{{ item | delete_cart_item, delete: 'Remove', class: 'btn-remove' }}
</div>
</div>
{% endfor %}
</div>
<div class="cart-summary">
<dl class="cart-totals">
<dt>Subtotal</dt>
<dd>{{ cart.subtotal_price | money_with_currency }}</dd>
{% if cart.shipping_price > 0 %}
<dt>Shipping</dt>
<dd>{{ cart.shipping_price | money_with_currency }}</dd>
{% endif %}
{% if cart.taxes > 0 %}
<dt>Tax</dt>
<dd>{{ cart.taxes | money_with_currency }}</dd>
{% endif %}
<dt class="total">Total</dt>
<dd class="total">{{ cart.grand_total | money_with_currency }}</dd>
</dl>
{{ cart | checkout_button, label: 'Proceed to Checkout', class: 'btn btn-primary btn-lg' }}
</div>
{% else %}
<p>Your cart is empty.</p>
<a href="/shop" class="btn btn-primary">Continue Shopping</a>
{% endif %}
</div>
</div>Update Cart Item
<!-- With quantity buttons -->
{{ item | update_cart_item, show_quantity_buttons: true }}
<!-- With dropdown -->
{{ item | update_cart_item, show_quantity_dropdown: true }}
<!-- With refresh button -->
{{ item | update_cart_item,
show_refresh_btn: true,
refresh_btn_label: 'Update',
refresh_btn_class: 'btn-update'
}}Delete Cart Item
{{ item | delete_cart_item }}
{{ item | delete_cart_item, delete: 'Remove', confirm: 'Are you sure?', class: 'btn-danger' }}Coupons
Apply coupons automatically:
<!-- Auto-apply on page load -->
{{ 'SUMMER2024' | auto_apply_coupon }}
<!-- Coupon form -->
<form action="/cart/apply_coupon" method="post">
<input type="hidden" name="authenticity_token" value="{{ auth_token }}">
<input type="text" name="coupon_code" placeholder="Coupon code">
<button type="submit">Apply</button>
</form>Checkout
The checkout flow is handled by Nimbu with customizable templates.
Checkout Template
templates/checkout.liquid:
<div class="checkout-page">
<div class="container">
<h1>Checkout</h1>
<div class="checkout-grid">
<!-- Order Summary -->
<div class="order-summary">
<h2>Order Summary</h2>
<div class="summary-items">
{% for item in cart.items %}
<div class="summary-item">
<span class="item-name">{{ item.product.name }} × {{ item.quantity }}</span>
<span class="item-price">{{ item.line_price | money_with_currency }}</span>
</div>
{% endfor %}
</div>
<dl class="summary-totals">
<dt>Subtotal</dt>
<dd>{{ cart.subtotal_price | money_with_currency }}</dd>
<dt>Shipping</dt>
<dd>{{ cart.shipping_price | money_with_currency }}</dd>
<dt>Tax</dt>
<dd>{{ cart.taxes | money_with_currency }}</dd>
<dt class="total">Total</dt>
<dd class="total">{{ cart.grand_total | money_with_currency }}</dd>
</dl>
</div>
<!-- Payment Form -->
<div class="payment-form">
{{ cart | payment_form,
title: 'Payment Information',
button_class: 'btn btn-primary btn-lg'
}}
</div>
</div>
</div>
</div>Payment Form
{{ cart | payment_form }}
{{ cart | payment_form,
id: 'payment-form',
class: 'checkout-form',
title: 'Payment Information',
value: 'Complete Purchase',
button_class: 'btn-checkout'
}}Product Listings
Shop Homepage
templates/shop/index.liquid:
<div class="shop-page">
<div class="container">
<header class="shop-header">
<h1>{% translate 'shop.title', default: 'Shop' %}</h1>
</header>
<!-- Featured Products -->
<section class="featured-products">
<h2>Featured Products</h2>
<div class="product-grid">
{% for product in products.featured limit: 4 %}
{% include 'product-card', product: product %}
{% endfor %}
</div>
</section>
<!-- Product Collections -->
<section class="collections">
<h2>Shop by Category</h2>
<div class="collection-grid">
{% for collection in collections.all %}
<a href="{{ collection.url }}" class="collection-card">
<h3>{{ collection.name }}</h3>
<p>{{ collection.product_count }} products</p>
</a>
{% endfor %}
</div>
</section>
<!-- All Products -->
<section class="all-products">
<h2>All Products</h2>
<div class="product-grid">
{% paginate products.all by 12 %}
{% for product in products.all %}
{% include 'product-card', product: product %}
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}
</div>
</section>
</div>
</div>Product Card Snippet
snippets/product-card.liquid:
<div class="product-card">
<a href="{{ product.url }}" class="product-link">
<div class="product-image">
{% if product.images.first %}
<img src="{{ product.images.first.url | filter, width: '400px', cropping: 'fill' }}"
alt="{{ product.name }}"
loading="lazy">
{% else %}
<img src="{{ 'product-placeholder.png' | theme_image_url }}" alt="{{ product.name }}">
{% endif %}
{% if product.on_sale %}
<span class="badge badge-sale">Sale</span>
{% endif %}
</div>
<div class="product-details">
<h3 class="product-name">{{ product.name }}</h3>
{% if product.vendor %}
<p class="product-vendor">{{ product.vendor }}</p>
{% endif %}
<div class="product-price">
{% if product.on_sale %}
<span class="sale-price">{{ product.on_sale_price | money_with_currency }}</span>
<span class="original-price">{{ product.price | money_with_currency }}</span>
{% else %}
<span class="price">{{ product.price | money_with_currency }}</span>
{% endif %}
</div>
</div>
</a>
<button class="btn-quick-add" data-product-id="{{ product.id }}">
Quick Add
</button>
</div>Advanced E-commerce Features
Customer-Specific Pricing
{% if product.advanced_pricing_enabled? %}
<p class="customer-price">
Your price: {{ product.price_for_current_customer | money_with_currency }}
</p>
{% endif %}Aggregated Products
Products with subproducts:
{% if product.aggregated? %}
<div class="subproducts">
<h3>Choose Options</h3>
{% for subproduct in product.subproducts %}
<div class="subproduct">
<h4>{{ subproduct.name }}</h4>
<p>{{ subproduct.price | money_with_currency }}</p>
{{ subproduct | add_to_cart }}
</div>
{% endfor %}
</div>
{% endif %}Stock Notifications
{% if product.current_stock == 0 %}
<form class="notify-form" action="/notify" method="post">
<p>Out of stock. Get notified when available:</p>
<input type="email" name="email" placeholder="Your email" required>
<input type="hidden" name="product_id" value="{{ product.id }}">
<button type="submit">Notify Me</button>
</form>
{% elsif product.current_stock < 5 %}
<p class="low-stock">Only {{ product.current_stock }} left in stock!</p>
{% endif %}SEO for E-commerce
Product Schema.org Markup
<div itemscope itemtype="https://schema.org/Product">
<meta itemprop="name" content="{{ product.name }}">
<meta itemprop="description" content="{{ product.description | strip_html | truncate: 200 }}">
<meta itemprop="image" content="{{ product.images.first.url }}">
<div itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<meta itemprop="price" content="{{ product.effective_price | money_without_currency }}">
<meta itemprop="priceCurrency" content="{{ shop.currency }}">
{% if product.status == 'sold_out' %}
<link itemprop="availability" href="https://schema.org/SoldOut">
{% else %}
<link itemprop="availability" href="https://schema.org/InStock">
{% endif %}
</div>
</div>Best Practices
1. Always Check Stock
{% if product.current_stock > 0 %}
<!-- Show add to cart -->
{% else %}
<!-- Show out of stock message -->
{% endif %}2. Handle Empty Cart
{% if cart.items.size > 0 %}
<!-- Show cart -->
{% else %}
<p>Your cart is empty.</p>
{% endif %}3. Optimize Product Images
<img src="{{ product.image.url | filter, width: '800px', cropping: 'fill' }}" loading="lazy">4. Use Pagination for Large Catalogs
{% paginate products.all by 24 %}
<!-- Products -->
{{ paginate | default_pagination }}
{% endpaginate %}5. Cache Product Grids
{% cache 'featured-products', expires_in: 3600 %}
{% for product in products.featured %}
{% include 'product-card', product: product %}
{% endfor %}
{% endcache %}Next Steps
- Channels - Custom content types with scope/filtering
- Filters - E-commerce - All cart/checkout filters
- Filters - Money - Price formatting
- Global Variables - Complete products/cart reference
Build powerful e-commerce experiences with Nimbu!