Liquid Basics

Liquid is the templating language that powers every Shopify theme. It bridges your HTML/CSS skills with Shopify's dynamic data — products, collections, cart, and more. This chapter teaches you everything you need to read, write, and debug Liquid code confidently.

📌 Chapter Overview

Time estimate: 60 – 90 minutes
Tags: Liquid HTML

What Is Liquid?

Liquid is a template language created by Shopify and written in Ruby. It's designed to be safe (no arbitrary code execution), readable (close to natural language), and flexible (handles complex data structures).

Liquid has three fundamental building blocks:

📦

Objects

Output data using double curly braces. Objects represent store data like products, collections, and settings.

🏷️

Tags

Control flow and logic using curly braces with percent signs. Tags handle conditionals, loops, assignments, and more.

🔧

Filters

Transform output using the pipe character. Filters modify strings, numbers, dates, URLs, and more.

Objects — Outputting Data

Objects are the simplest Liquid concept. They output data to the page using double curly braces {{ }}.

Basic Object Output
<!-- Store name -->
<h1>{{ shop.name }}</h1>

<!-- Product title -->
<h2>{{ product.title }}</h2>

<!-- Product price (formatted as money) -->
<span>{{ product.price | money }}</span>

<!-- Collection description -->
<div>{{ collection.description }}</div>

<!-- Current page title -->
<title>{{ page_title }}</title>

Objects use dot notation to access properties, just like JavaScript. product.title accesses the title property of the product object.

Important Global Objects

These objects are available on every page of the theme:

Object Description Example Properties
shop The entire store shop.name, shop.url, shop.currency, shop.email
settings Global theme settings settings.color_primary, settings.font_body
cart Current shopping cart cart.item_count, cart.total_price, cart.items
request Current request info request.locale, request.host, request.path
linklists Navigation menus linklists.main-menu.links
page_title Current page's title Used in <title> tags
canonical_url Canonical URL of current page Used for SEO <link rel="canonical">
content_for_header Shopify-injected scripts Required in <head>

Page-Specific Objects

These objects are only available on their respective page types:

Page Type Available Object Key Properties
Product page product .title, .price, .description, .images, .variants, .url, .vendor, .type, .tags
Collection page collection .title, .products, .description, .products_count, .image
Cart page cart .items, .total_price, .item_count, .note
Blog page blog .title, .articles, .tags
Article page article .title, .content, .author, .published_at, .image
Search results search .terms, .results, .results_count
Custom page page .title, .content, .url
💡 How to Explore Objects

When you're unsure what properties an object has, use the Shopify Liquid Reference. Bookmark it — you'll use it daily. You can also output an entire object for debugging: {{ product | json }} will show all the product's data as JSON.

Tags — Logic and Control Flow

Tags use curly braces with percent signs {% %}. They don't output anything visible — they control what gets rendered and how.

Conditionals: if, elsif, else, unless

Conditional Examples
<!-- Basic if/else -->
{% if product.available %}
  <button type="submit">Add to Cart</button>
{% else %}
  <button disabled>Sold Out</button>
{% endif %}

<!-- Multiple conditions with elsif -->
{% if cart.item_count == 0 %}
  <p>Your cart is empty</p>
{% elsif cart.item_count == 1 %}
  <p>You have 1 item in your cart</p>
{% else %}
  <p>You have {{ cart.item_count }} items in your cart</p>
{% endif %}

<!-- Unless (opposite of if) -->
{% unless product.title == blank %}
  <h1>{{ product.title }}</h1>
{% endunless %}

<!-- Checking if a value exists -->
{% if product.featured_image %}
  <img src="{{ product.featured_image | image_url: width: 600 }}"
       alt="{{ product.featured_image.alt | escape }}">
{% endif %}

<!-- Compound conditions -->
{% if product.available and product.price > 0 %}
  <span class="price">{{ product.price | money }}</span>
{% endif %}

{% if product.type == 'shirt' or product.type == 'pants' %}
  <span class="badge">Clothing</span>
{% endif %}

Comparison Operators

Operator Meaning Example
== Equals {% if product.type == 'shirt' %}
!= Not equals {% if product.vendor != 'Nike' %}
> Greater than {% if product.price > 5000 %}
< Less than {% if cart.item_count < 3 %}
>= Greater or equal {% if product.variants.size >= 2 %}
<= Less or equal {% if collection.products_count <= 10 %}
contains String/array includes {% if product.tags contains 'sale' %}
and Logical AND {% if a and b %}
or Logical OR {% if a or b %}
⚠️ Prices Are in Cents

Shopify stores prices in cents (or the smallest currency unit). So $50.00 is stored as 5000. When comparing prices, use the cents value: {% if product.price > 5000 %}. Use the money filter for display: {{ product.price | money }}.

Case / When

Case Statement
{% case template.name %}
  {% when 'product' %}
    <!-- Product page specific content -->
  {% when 'collection' %}
    <!-- Collection page specific content -->
  {% when 'index' %}
    <!-- Home page specific content -->
  {% else %}
    <!-- Default content -->
{% endcase %}

Loops — Iterating Over Data

Loops are essential in Shopify themes. You'll loop through products, collections, images, variants, cart items, navigation links, and more.

The for Loop

Basic For Loop
<!-- Loop through products in a collection -->
<div class="product-grid">
  {% for product in collection.products %}
    <div class="product-card">
      <h3>{{ product.title }}</h3>
      <span>{{ product.price | money }}</span>
    </div>
  {% endfor %}
</div>

<!-- Loop through product images -->
<div class="product-gallery">
  {% for image in product.images %}
    <img
      src="{{ image | image_url: width: 800 }}"
      alt="{{ image.alt | escape }}"
      loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
    >
  {% endfor %}
</div>

<!-- Loop through navigation links -->
<nav>
  <ul>
    {% for link in linklists.main-menu.links %}
      <li>
        <a href="{{ link.url }}"
           {% if link.active %} class="active"{% endif %}>
          {{ link.title }}
        </a>
      </li>
    {% endfor %}
  </ul>
</nav>

The forloop Object

Inside every for loop, Liquid provides the special forloop object with useful properties:

Property Description Example Use
forloop.index Current iteration (1-based) Display item number
forloop.index0 Current iteration (0-based) CSS class names
forloop.first true on first iteration Eager-load first image
forloop.last true on last iteration Skip trailing comma
forloop.length Total number of iterations Show "X items" count
Using forloop Properties
{% for product in collection.products %}
  <div class="product-card
    {% if forloop.first %} product-card--featured{% endif %}
    {% if forloop.last %} product-card--last{% endif %}"
  >
    <span class="product-number">{{ forloop.index }} of {{ forloop.length }}</span>
    <h3>{{ product.title }}</h3>
  </div>
{% endfor %}

Limit and Offset

Limit and Offset
<!-- Show only the first 4 products -->
{% for product in collection.products limit: 4 %}
  <div class="product-card">{{ product.title }}</div>
{% endfor %}

<!-- Skip the first 2 products, show next 4 -->
{% for product in collection.products offset: 2 limit: 4 %}
  <div class="product-card">{{ product.title }}</div>
{% endfor %}

<!-- Loop through a number range -->
{% for i in (1..5) %}
  <span>Star {{ i }}</span>
{% endfor %}

Empty State with else

For Loop with Else
<!-- Handle empty collections gracefully -->
{% for product in collection.products %}
  <div class="product-card">
    <h3>{{ product.title }}</h3>
  </div>
{% else %}
  <p class="empty-state">No products found in this collection.</p>
{% endfor %}
✅ Best Practice

Always handle empty states. Use {% else %} in for loops or check {% if collection.products.size > 0 %} before rendering grids. A blank page with no feedback is a terrible user experience.

Filters — Transforming Output

Filters modify the output of objects. They use the pipe character | and can be chained together.

String Filters

String Filters
<!-- Capitalize -->
{{ 'hello world' | capitalize }}
<!-- Output: Hello world -->

<!-- Upcase / Downcase -->
{{ product.title | upcase }}
{{ product.title | downcase }}

<!-- Replace -->
{{ product.title | replace: ' ', '-' }}

<!-- Truncate -->
{{ product.description | strip_html | truncate: 100 }}
<!-- Strips HTML tags, then limits to 100 characters -->

<!-- Strip HTML -->
{{ product.description | strip_html }}

<!-- Escape (for use in attributes) -->
<img alt="{{ product.title | escape }}">

<!-- Append / Prepend -->
{{ 'world' | prepend: 'hello ' }}
<!-- Output: hello world -->

<!-- Handle blank values -->
{{ product.vendor | default: 'Unknown Vendor' }}

Number Filters

Number Filters
<!-- Math operations -->
{{ 100 | plus: 50 }}         <!-- 150 -->
{{ 100 | minus: 30 }}        <!-- 70 -->
{{ 10 | times: 5 }}          <!-- 50 -->
{{ 100 | divided_by: 3 }}    <!-- 33 -->
{{ 100 | modulo: 3 }}        <!-- 1 -->

<!-- Round -->
{{ 4.567 | round: 2 }}       <!-- 4.57 -->
{{ 4.567 | ceil }}            <!-- 5 -->
{{ 4.567 | floor }}           <!-- 4 -->

Money Filters

Money Filters
<!-- Format price as money (uses store's format) -->
{{ product.price | money }}
<!-- Output: $25.00 -->

<!-- Money with currency code -->
{{ product.price | money_with_currency }}
<!-- Output: $25.00 USD -->

<!-- Money without trailing zeros -->
{{ product.price | money_without_trailing_zeros }}
<!-- Output: $25 (if price is $25.00) -->

<!-- Calculate sale percentage -->
{% if product.compare_at_price > product.price %}
  {% assign discount = product.compare_at_price | minus: product.price %}
  {% assign percentage = discount | times: 100 | divided_by: product.compare_at_price %}
  <span class="sale-badge">-{{ percentage }}%</span>
{% endif %}

URL Filters

URL Filters
<!-- Asset URL (for files in assets/ folder) -->
{{ 'style.css' | asset_url }}
<!-- Output: //cdn.shopify.com/s/files/.../style.css -->

<!-- Stylesheet tag -->
{{ 'base.css' | asset_url | stylesheet_tag }}
<!-- Output: <link href="..." rel="stylesheet"> -->

<!-- Script tag -->
{{ 'global.js' | asset_url | script_tag }}
<!-- Output: <script src="..."></script> -->

<!-- Product image URL with size -->
{{ product.featured_image | image_url: width: 600 }}
{{ product.featured_image | image_url: width: 600, height: 400, crop: 'center' }}

<!-- Link to a specific page type -->
{{ product.url }}            <!-- /products/product-handle -->
{{ collection.url }}         <!-- /collections/collection-handle -->
{{ page.url }}               <!-- /pages/page-handle -->

Image URL Filter (Critical Knowledge)

The image_url filter is one of the most important filters in Shopify theme development. It generates optimized image URLs:

Image URL Filter
<!-- Basic responsive image -->
<img
  src="{{ product.featured_image | image_url: width: 800 }}"
  srcset="
    {{ product.featured_image | image_url: width: 400 }} 400w,
    {{ product.featured_image | image_url: width: 800 }} 800w,
    {{ product.featured_image | image_url: width: 1200 }} 1200w
  "
  sizes="(max-width: 768px) 100vw, 50vw"
  alt="{{ product.featured_image.alt | escape }}"
  width="{{ product.featured_image.width }}"
  height="{{ product.featured_image.height }}"
  loading="lazy"
>

<!-- With cropping -->
{{ image | image_url: width: 400, height: 400, crop: 'center' }}

<!-- Available crop values: top, center, bottom, left, right -->
💡 Always Include width and height Attributes

Always set width and height attributes on images. This prevents Cumulative Layout Shift (CLS) — a Core Web Vital metric. Without dimensions, the browser doesn't know how much space to reserve, causing content to jump as images load.

Filter Chaining

Filters can be chained — the output of one filter becomes the input of the next:

Filter Chaining
<!-- Chain multiple filters -->
{{ product.description | strip_html | truncate: 150 | escape }}

<!-- Build a CSS class from a product title -->
{{ product.title | downcase | replace: ' ', '-' | prepend: 'product-' }}
<!-- "Summer Shirt" becomes "product-summer-shirt" -->

<!-- Format a date -->
{{ article.published_at | date: '%B %d, %Y' }}
<!-- Output: January 15, 2024 -->

Variables and Assignment

You can create local variables using assign and capture.

assign

Assign Variables
<!-- Simple assignment -->
{% assign featured_product = collections.frontpage.products.first %}
<h2>{{ featured_product.title }}</h2>

<!-- Boolean flag -->
{% assign show_sale_badge = false %}
{% if product.compare_at_price > product.price %}
  {% assign show_sale_badge = true %}
{% endif %}

{% if show_sale_badge %}
  <span class="badge badge--sale">Sale</span>
{% endif %}

<!-- Calculate values -->
{% assign columns = section.settings.columns %}
{% assign column_width = 100 | divided_by: columns %}

<!-- String assignment with filter -->
{% assign product_handle = product.title | handleize %}
<div id="{{ product_handle }}">...</div>

capture

capture stores a block of rendered content into a variable:

Capture Tag
<!-- Capture complex HTML into a variable -->
{% capture price_html %}
  {% if product.compare_at_price > product.price %}
    <s class="price--compare">{{ product.compare_at_price | money }}</s>
    <span class="price--sale">{{ product.price | money }}</span>
  {% else %}
    <span class="price">{{ product.price | money }}</span>
  {% endif %}
{% endcapture %}

<!-- Use the captured content -->
<div class="product-price">{{ price_html }}</div>
💡 When to Use assign vs. capture

Use assign for simple values (strings, numbers, object references). Use capture when you need to store rendered HTML or complex string concatenations. capture processes Liquid inside it before storing the result.

render vs. include

Both tags insert snippets, but they work differently:

render vs. include
<!-- RENDER (recommended — creates isolated scope) -->
{% render 'product-card', product: product, show_vendor: true %}

<!-- INCLUDE (legacy — shares parent scope) -->
{% include 'product-card' %}
Feature render include
Variable scope Isolated (must pass variables explicitly) Shared (accesses parent variables)
Performance ✓ Better (can be cached) ✗ Slower
Recommended ✓ Yes ✗ Deprecated
Predictability High (no side effects) Low (can create unexpected variable conflicts)
🚨 Always Use render, Not include

include is deprecated in Shopify themes. Always use render. It creates an isolated scope, which means variables from the parent template don't leak into the snippet and vice versa. This makes your code predictable and prevents subtle bugs.

Passing Variables to Snippets
<!-- In your section: pass all needed variables explicitly -->
{% for product in collection.products %}
  {% render 'product-card',
    product: product,
    show_vendor: section.settings.show_vendor,
    show_sale_badge: true,
    image_size: 400,
    lazy_load: true
  %}
{% endfor %}

<!-- In snippets/product-card.liquid: use the passed variables -->
<div class="product-card">
  <img
    src="{{ product.featured_image | image_url: width: image_size }}"
    alt="{{ product.featured_image.alt | escape }}"
    {% if lazy_load %}loading="lazy"{% endif %}
  >
  <h3>{{ product.title }}</h3>
  {% if show_vendor %}
    <span>{{ product.vendor }}</span>
  {% endif %}
  <span>{{ product.price | money }}</span>
</div>

Whitespace Control

Liquid tags produce whitespace in the output HTML. Use the hyphen syntax to strip whitespace:

Whitespace Control
<!-- Without whitespace control (leaves blank lines) -->
{% if product.available %}
  <span>In Stock</span>
{% endif %}

<!-- With whitespace control (clean output) -->
{%- if product.available -%}
  <span>In Stock</span>
{%- endif -%}

<!-- Also works with output tags -->
{{- product.title -}}

The hyphens (-) strip whitespace from the side they're on:

  • {%- strips whitespace before the tag
  • -%} strips whitespace after the tag
  • {%- -%} strips both sides
✅ Best Practice

Use whitespace-stripping hyphens ({%- -%}) on logic tags like if, for, assign, and capture. This keeps your rendered HTML clean without random blank lines. Dawn uses this pattern extensively — study its code for examples.

Comments

Liquid Comments
<!-- HTML comment: visible in page source -->

{% comment %}
  Liquid comment: NOT visible in page source.
  Use these for developer notes.
{% endcomment %}

{%- comment -%}
  With whitespace stripping.
  This is the preferred style.
{%- endcomment -%}

{% # Inline comment (Shopify Liquid only, newer syntax) %}
💡 Comment Strategy

Use Liquid comments ({% comment %}) for developer notes — they won't appear in the page source. Use HTML comments sparingly, as they increase page size and are visible to anyone who views the source.

The section Object

Inside every section file, you have access to the section object. This is how you access the settings and blocks defined in the schema:

Section Object Usage
<!-- Access section settings -->
<h2>{{ section.settings.heading }}</h2>

<!-- Unique section ID (for CSS scoping) -->
<div id="section-{{ section.id }}">
  ...
</div>

<!-- Loop through blocks -->
{% for block in section.blocks %}
  <div {{ block.shopify_attributes }}>
    {% case block.type %}
      {% when 'heading' %}
        <h2>{{ block.settings.heading }}</h2>
      {% when 'text' %}
        <p>{{ block.settings.text }}</p>
      {% when 'button' %}
        <a href="{{ block.settings.link }}" class="btn">
          {{ block.settings.label }}
        </a>
    {% endcase %}
  </div>
{% endfor %}

Practical Liquid Patterns

Here are real-world patterns you'll use constantly in theme development:

Sale Price Display

Sale Price Pattern
{%- if product.compare_at_price > product.price -%}
  <div class="price price--on-sale">
    <s class="price__compare">{{ product.compare_at_price | money }}</s>
    <span class="price__current">{{ product.price | money }}</span>
    {%- assign savings = product.compare_at_price | minus: product.price -%}
    <span class="price__savings">Save {{ savings | money }}</span>
  </div>
{%- else -%}
  <div class="price">
    <span class="price__current">{{ product.price | money }}</span>
  </div>
{%- endif -%}

Responsive Image with srcset

Responsive Image Pattern
{%- if product.featured_image -%}
  {%- assign image = product.featured_image -%}
  <img
    src="{{ image | image_url: width: 800 }}"
    srcset="
      {{ image | image_url: width: 200 }} 200w,
      {{ image | image_url: width: 400 }} 400w,
      {{ image | image_url: width: 600 }} 600w,
      {{ image | image_url: width: 800 }} 800w,
      {{ image | image_url: width: 1000 }} 1000w
    "
    sizes="(max-width: 749px) calc(100vw - 40px), (max-width: 999px) 50vw, 33vw"
    alt="{{ image.alt | escape }}"
    width="{{ image.width }}"
    height="{{ image.height }}"
    loading="lazy"
  >
{%- else -%}
  <div class="placeholder-image">
    {{ 'product-1' | placeholder_svg_tag: 'placeholder' }}
  </div>
{%- endif -%}

Active Navigation Link

Active Nav Link Pattern
<nav class="main-nav">
  <ul>
    {%- for link in linklists.main-menu.links -%}
      <li class="nav-item">
        <a
          href="{{ link.url }}"
          class="nav-link{% if link.active %} nav-link--active{% endif %}{% if link.child_active %} nav-link--child-active{% endif %}"
          {% if link.current %} aria-current="page"{% endif %}
        >
          {{ link.title | escape }}
        </a>

        {%- if link.links.size > 0 -%}
          <ul class="nav-dropdown">
            {%- for child_link in link.links -%}
              <li>
                <a href="{{ child_link.url }}"
                   class="nav-dropdown__link{% if child_link.active %} active{% endif %}">
                  {{ child_link.title | escape }}
                </a>
              </li>
            {%- endfor -%}
          </ul>
        {%- endif -%}
      </li>
    {%- endfor -%}
  </ul>
</nav>

Pagination

Pagination Pattern
{%- paginate collection.products by 12 -%}
  <div class="product-grid">
    {%- for product in collection.products -%}
      {% render 'product-card', product: product %}
    {%- endfor -%}
  </div>

  {%- if paginate.pages > 1 -%}
    <nav class="pagination" aria-label="Pagination">
      {%- if paginate.previous -%}
        <a href="{{ paginate.previous.url }}" aria-label="Previous page">
          &larr; Previous
        </a>
      {%- endif -%}

      {%- for part in paginate.parts -%}
        {%- if part.is_link -%}
          <a href="{{ part.url }}">{{ part.title }}</a>
        {%- else -%}
          <span class="current" aria-current="page">{{ part.title }}</span>
        {%- endif -%}
      {%- endfor -%}

      {%- if paginate.next -%}
        <a href="{{ paginate.next.url }}" aria-label="Next page">
          Next &rarr;
        </a>
      {%- endif -%}
    </nav>
  {%- endif -%}
{%- endpaginate -%}
⚠️ Pagination Is Required

Shopify limits the number of products returned in a single loop to 50. Always wrap collection product loops in {% paginate %} tags. Without pagination, stores with more than 50 products will have missing items.

Debugging Liquid

Debugging Liquid can be challenging since there's no console or debugger. Here are professional techniques:

Output as JSON

Debug with JSON
<!-- Output any object as JSON for inspection -->
<pre>{{ product | json }}</pre>

<!-- Check section settings -->
<pre>{{ section.settings | json }}</pre>

<!-- Check what blocks exist -->
<pre>{{ section.blocks | json }}</pre>

<!-- Inspect a specific variable -->
{% assign my_var = product.variants.first %}
<pre>{{ my_var | json }}</pre>

<!-- Wrap in a details element to collapse -->
<details>
  <summary>Debug: product data</summary>
  <pre style="font-size: 12px; max-height: 400px; overflow: auto;">
    {{ product | json }}
  </pre>
</details>

Debug Comments

Debug Comments
<!-- DEBUG: product.available = {{ product.available }} -->
<!-- DEBUG: collection.products.size = {{ collection.products.size }} -->
<!-- DEBUG: section.blocks.size = {{ section.blocks.size }} -->
<!-- DEBUG: template.name = {{ template.name }} -->
✅ My Debugging Workflow

1. Check if the object exists: {{ product | json }}
2. Check specific properties: {{ product.title }}
3. Test conditions separately: wrap if conditions in HTML comments
4. Use the browser's "View Source" to see rendered output
5. Check the Shopify CLI terminal for Liquid errors
6. Remove debug output before committing!

Common Liquid Mistakes

Mistake Problem Fix
{{ product.price }} without money filter Shows price in cents (e.g., "2500" instead of "$25.00") {{ product.price | money }}
Using include instead of render Variable scope leaks, harder to debug Always use {% render %}
Missing | escape on user content Potential XSS vulnerability in alt tags, attributes {{ product.title | escape }} in attributes
Forgetting {% endfor %} or {% endif %} Liquid error — page won't render Always close your tags immediately after opening
Not handling blank/nil values Empty HTML elements, broken layouts Check with {% if value != blank %} or use | default:
No paginate wrapper on collection loops Only first 50 products shown Always use {% paginate collection.products by 12 %}

Quick Reference Cheat Sheet

Liquid Cheat Sheet
╔══════════════════════════════════════════════════════════╗
║  LIQUID CHEAT SHEET FOR SHOPIFY THEMES                   ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  OUTPUT:     {{ object.property }}                        ║
║  FILTER:     {{ value | filter_name }}                    ║
║  LOGIC:      {% if condition %} ... {% endif %}           ║
║  LOOP:       {% for item in array %} ... {% endfor %}     ║
║  ASSIGN:     {% assign var = value %}                     ║
║  CAPTURE:    {% capture var %} ... {% endcapture %}       ║
║  RENDER:     {% render 'snippet', var: value %}           ║
║  COMMENT:    {% comment %} ... {% endcomment %}           ║
║  STRIP WS:   {%- tag -%}  or  {{- output -}}             ║
║  PAGINATE:   {% paginate array by 12 %} ... {% endpag %}  ║
║                                                          ║
║  COMMON FILTERS:                                         ║
║  | money              Format as currency                 ║
║  | image_url: width:  Generate image URL                 ║
║  | asset_url          CDN URL for asset files             ║
║  | escape             HTML-safe output                   ║
║  | strip_html         Remove HTML tags                    ║
║  | truncate: 100      Limit string length                ║
║  | date: '%B %d, %Y'  Format dates                       ║
║  | default: 'fallback' Default value if blank             ║
║  | handleize          URL-safe string                    ║
║  | json               Output as JSON                     ║
║  | stylesheet_tag     Wrap in <link> tag                 ║
║  | script_tag         Wrap in <script> tag               ║
║                                                          ║
║  SECTION ACCESS:                                         ║
║  section.id           Unique section ID                  ║
║  section.settings     Section schema settings             ║
║  section.blocks       Section blocks array                ║
║  block.type           Block type identifier              ║
║  block.settings       Block schema settings               ║
║  block.shopify_attributes  Required for theme editor     ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝

Key Takeaways

  • Liquid has three building blocks: objects ({{ }}), tags ({% %}), and filters (|)
  • Global objects like shop, settings, cart are available everywhere
  • Page-specific objects like product and collection depend on the page type
  • Use for loops with forloop properties for iteration
  • Always handle empty states with {% else %} in loops
  • Prices are in cents — always use the money filter for display
  • Use render not include for snippets
  • Use whitespace-stripping hyphens {%- -%} on logic tags
  • Debug with | json filter and HTML comments
  • Always use paginate for collection product loops

With Liquid fundamentals in place, you're ready to explore the complete folder structure of a Shopify theme and understand what every file does.