capture for building strings

{% capture product_class %}
  product-card
  {% if product.available %}in-stock{% else %}sold-out{% endif %}
  {% if product.compare_at_price > product.price %}on-sale{% endif %}
{% endcapture %}

<div class="{{ product_class | strip | split: ' ' | join: ' ' }}">

Use capture to build class strings or HTML fragments conditionally. The filter chain removes extra whitespace from the multiline capture.

for loop with forloop object

{% for variant in product.variants %}
  {% unless forloop.first %},{% endunless %}
  {{ variant.title }}
  {% comment %} forloop.index, forloop.last, forloop.rindex also available {% endcomment %}
{% endfor %}

assign vs capture

Use assign for simple values: {% assign price = product.price | money %}. Use capture when the value spans multiple lines or includes Liquid logic. Never use capture for simple string assignments.

section settings schema

{
  "type": "text",
  "id": "heading",
  "label": "Heading",
  "default": "Featured Products"
},
{
  "type": "range",
  "id": "products_count",
  "min": 2, "max": 12, "step": 2,
  "default": 4,
  "label": "Number of products"
}

the unless alternative to if-not

Prefer {% unless condition %} over {% if condition == false %} — it's shorter, reads like English, and avoids negation errors in complex conditions.

tablerow for grid rendering

Liquid's tablerow tag renders table rows with automatic column counting: {% tablerow product in collection.products cols:3 %}. Less common now that CSS grid is standard, but useful for email templates or legacy layouts.