Skip to content

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

PropertyDescription
idUnique product ID
nameProduct name
descriptionHTML description
skuStock keeping unit
priceBase price (cents)
on_saleBoolean: product on sale
on_sale_priceSale price if on sale
effective_priceCurrent price (sale or regular)
price_for_current_customerCustomer-specific pricing
urlProduct page URL
status"active", "sold_out", "hidden"
current_stockAvailable inventory
amount_in_cartQuantity in customer's cart
imagesArray of product images
variantsArray of product variants
vendorBrand/manufacturer name
typeProduct type/category
weightWeight in grams
tax_schemeTax configuration
seo_title, seo_descriptionSEO metadata
Custom attributesAny custom fields defined

Product Template

templates/product.liquid:

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.):

liquid
<!-- 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

PropertyDescription
idVariant ID
nameVariant name (e.g., "Size 42")
priceVariant-specific price
on_sale_priceSale price if applicable
skuVariant SKU
stockAvailable inventory
weightWeight in grams
urlProduct URL with variant selected

Collections

Collections group related products together.

Collection Template

templates/collection.liquid:

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

PropertyDescription
nameCollection name
descriptionHTML description
slugURL slug
urlCollection page URL
productsProducts in collection
product_countNumber of products
current?Boolean: viewing this collection

Filtering Products

Use {% scope %} to filter products:

liquid
<!-- 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

PropertyDescription
itemsArray of line items
items_countTotal items
total_priceTotal before shipping/tax
total_weightTotal weight
subtotal_priceSubtotal
shipping_priceShipping cost
taxesTax amount
grand_totalFinal total with shipping/taxes

Add to Cart

Use the add_to_cart filter:

liquid
<!-- 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:

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

liquid
<!-- 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

liquid
{{ item | delete_cart_item }}

{{ item | delete_cart_item, delete: 'Remove', confirm: 'Are you sure?', class: 'btn-danger' }}

Coupons

Apply coupons automatically:

liquid
<!-- 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:

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

liquid
{{ 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:

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:

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

liquid
{% 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:

liquid
{% 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

liquid
{% 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

liquid
<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

liquid
{% if product.current_stock > 0 %}
  <!-- Show add to cart -->
{% else %}
  <!-- Show out of stock message -->
{% endif %}

2. Handle Empty Cart

liquid
{% if cart.items.size > 0 %}
  <!-- Show cart -->
{% else %}
  <p>Your cart is empty.</p>
{% endif %}

3. Optimize Product Images

liquid
<img src="{{ product.image.url | filter, width: '800px', cropping: 'fill' }}" loading="lazy">

4. Use Pagination for Large Catalogs

liquid
{% paginate products.all by 24 %}
  <!-- Products -->
  {{ paginate | default_pagination }}
{% endpaginate %}

5. Cache Product Grids

liquid
{% cache 'featured-products', expires_in: 3600 %}
  {% for product in products.featured %}
    {% include 'product-card', product: product %}
  {% endfor %}
{% endcache %}

Next Steps

Build powerful e-commerce experiences with Nimbu!