# Basics

This section covers the fundamental concepts of Liquid that you need to understand before working with templates in Modyo.

# What Is Liquid?

Liquid is a template language that processes text and generates dynamic output. It works in two stages:

  1. Parse: Liquid reads your template and finds the Liquid code
  2. Render: Liquid executes the code and generates the final output

# Design principles

Liquid was designed with specific requirements:

  • Clean markup: Produces readable and maintainable HTML
  • Safe: Users can edit templates without executing unsafe code
  • Stateless: Compilation and rendering are separated for better performance
  • Flexible: Works with HTML, JSON, XML, emails and more

# Basic Syntax

Liquid uses two types of delimiters to distinguish between static content and dynamic code:

# Output

Double curly braces display the value of an expression:

{{ product.name }}
{{ 'Hello world' }}
{{ 100 | plus: 50 }}

Everything inside is evaluated and displayed on the page.

# Tags

Curly braces with percent {% %} execute logic but don't produce visible output:

{% if user %}
  Welcome!
{% endif %}

{% assign price = 100 %}

# Comments

Comments don't appear in the final output:

{%- comment -%}
  This is a comment that won't be seen
{%- endcomment -%}

# Basic Data Types

Liquid recognizes these data types:

  • String: Text in quotes ("text" or 'text')
  • Number: Integers or decimals (42, 3.14)
  • Boolean: True or false (true, false)
  • Nil: Absence of value (nil)
  • Array: List of values
  • Hash: Key-value pairs

For complete details on data types and operations, see Variables and Data Types.

# Truthy and Falsy Values

In Liquid, these values are considered falsy (false):

  • false
  • nil

Everything else is considered truthy (true), including:

  • true
  • Any number (even 0)
  • Any string (even "" empty)
  • Empty arrays []
  • Empty hashes {}
{% if 0 %}
  This WILL display (0 is truthy)
{% endif %}

{% if "" %}
  This WILL display (empty string is truthy)
{% endif %}

{% if nil %}
  This WON'T display (nil is falsy)
{% endif %}

# Whitespace Control

Liquid generates spaces and line breaks around code. You can control this with hyphens:

# Without whitespace control

{% assign name = "John" %}
Hello {{ name }}!

Output:


Hello John!

(Note the extra line break)

# With whitespace control

{%- assign name = "John" -%}
Hello {{ name }}!

Output:

Hello John!

Hyphens remove whitespace:

  • {{- or {%- removes whitespace to the left
  • -}} or -%} removes whitespace to the right

# Operators

# Comparison operators

Operator Description Example
== Equal to {% if price == 100 %}
!= Not equal to {% if status != 'active' %}
> Greater than {% if age > 18 %}
< Less than {% if stock < 10 %}
>= Greater or equal {% if points >= 1000 %}
<= Less or equal {% if discount <= 50 %}

# Logical operators

Operator Description Example
and Logical AND {% if price < 100 and stock > 0 %}
or Logical OR {% if vip or premium %}

# Contains operator

Checks if a string contains another or if an array contains an element:

{% if product.name contains "sale" %}
  On sale!
{% endif %}

{% assign fruits = "apple,pear,grape" | split: "," %}
{% if fruits contains "pear" %}
  Pears available
{% endif %}

# Order of Operations

Liquid doesn't follow traditional mathematical order. Operations are executed from right to left:

{{ 10 | plus: 5 | times: 2 }}

Executes as:

  1. 5 | times: 2 = 10
  2. 10 | plus: 10 = 20

To control order, use parentheses or intermediate variables:

{% assign temp = 10 | plus: 5 %}
{{ temp | times: 2 }}

Result: 30

# Working with Variables

# Creating variables with assign

The assign tag creates new variables:

{% assign name = "Mary" %}
{% assign age = 30 %}
{% assign active = true %}

# Capturing blocks with capture

The capture tag saves a block of content in a variable:

{% capture welcome %}
  Hello {{ user.name }}!
  Today is {{ 'now' | date: '%A' }}
{% endcapture %}

{{ welcome }}

# Global scope

Unlike other languages, variables in Liquid have global scope from their creation:

{% for item in items %}
  {% assign found = true %}
{% endfor %}

{{ found }} <!-- Available outside the loop -->

For more details on variables and data types, see Variables and Data Types. To learn about all available objects in Modyo, see Objects.

# Liquid vs. HTML

It's important to understand that Liquid is processed before HTML:

<!-- This WON'T work -->
<div class="{% if active %}active{% endif %}">
  <script>
    var name = {{ user.name }}; // Error: missing quotes
  </script>
</div>

<!-- This WILL work -->
<div class="{% if active %}active{% endif %}">
  <script>
    var name = "{{ user.name }}"; // Correct: with quotes
  </script>
</div>

# Escaping Liquid

If you need to display Liquid code without executing it:

{% raw %}
  {{ this won't be processed }}
  {% neither will this %}
{% endraw %}

Or use HTML entities:

&#123;&#123; product.name &#125;&#125;

# Common Errors

# 1. Undefined variables

<!-- Ambiguous: if user doesn't exist, this prints empty with no warning -->
{{ user.name.first }}

<!-- Better: validate first and handle the empty case -->
{% if user %}
  {{ user.name.first }}
{% endif %}

# 2. Type comparison

<!-- Wrong: comparing string with number -->
{% if age == "18" %}

<!-- Right: same type -->
{% if age == 18 %}

# 3. Spaces in names

<!-- Wrong: spaces in the name -->
{% assign my variable = 10 %}

<!-- Right: no spaces or with underscores -->
{% assign my_variable = 10 %}

# 4. Filters without pipe

<!-- Wrong: missing pipe -->
{{ text upcase }}

<!-- Right: with pipe -->
{{ text | upcase }}

# Render Error Behavior

Modyo checks your Liquid syntax when you save a template, but that check only covers parsing: it catches an unclosed tag or an invalid argument and stops you from saving. Anything that fails during rendering passes that check and reaches the published page.

At render time the platform runs in non-strict mode: rendering never breaks the page, it degrades it silently. These are the four symptoms you will run into and what each one means.

# A variable or attribute that doesn't exist prints empty

Variables aren't strict. A variable that isn't in the context, or an attribute the object doesn't have, raises no error: it prints as an empty string.

{{ page_titles }}
{{ page.made_up_title }}

Both lines produce exactly the same output as a legitimately empty value, so a misspelled name, data that hasn't loaded yet and a variable used outside its context are indistinguishable on the published page. When something doesn't show up, check the spelling first and then that the variable exists on that page type; see Context Variables.

# A filter that doesn't exist returns the value untransformed

Filters aren't strict either. If you write a filter the platform doesn't have, Liquid neither fails nor skips it: it returns the input value exactly as it arrived.

{{ "text" | filter_that_does_not_exist }}

That line prints text. The serious case is when the input value is a Modyo object instead of a string: since nothing transforms it, the page publishes the object's internal name, for example Liquid::Drops::Assets::VideoAssetDrop. There is no exception, no message and no visible hint of the cause. It is the symptom shared by misspelled filters and by filters that were removed from the platform; check the catalog in Filters.

# A render error leaves a comment in the HTML

When rendering does fail, Modyo catches the error, logs it server side and leaves the comment <!-- Liquid Error --> in the published HTML, right where it failed. The visitor sees no message: they see a gap.

Searching for Liquid Error in the published page source is the fastest way to locate the exact spot in the template that failed.

# The exception: indexing an identifier that doesn't exist

Some accesses don't render empty, they abort rendering. They are the ones that look an element up by its identifier:

  • spaces['uid'] and spaces['uid'].types['uid']
  • menus['slug']
  • fields['uid'] on an Origination task response

If the identifier matches nothing, you don't get an empty value: you get a render error and the comment in its place. There is no way to check beforehand, because any check goes through indexing. What you can do is contain the damage: group the access and its use inside the same {% if %}, so the error takes that whole block instead of leaving the comment in the middle of an HTML attribute and breaking the markup.

# Snippets can nest up to 99 levels

Snippet chains have a ceiling. You can nest up to 99 levels; the attempt to open level 100 doesn't run and the page publishes the literal text [Liquid Internal Error] Nesting too deep instead.

Seeing that text on a page almost always means a snippet includes itself, directly or indirectly. Review the {% snippet %} chain from the point where the message appears.

Attention

None of these four behaviors is caught when you save the template, because they all happen at render time. A template can be saved and published without a single warning and still print blanks, error comments or an object's internal name. See Errors in Views.

# Conventions and Best Practices

  1. Variable names: Use snake_case for variables

    {% assign total_price = 100 %}
    {% assign user_name = "John" %}
    
  2. Indentation: Indent content inside control tags

    {% if user %}
      {% for order in user.orders %}
        {{ order.id }}
      {% endfor %}
    {% endif %}
    
  3. Validation: Always validate before accessing nested properties

    {% if product.variants and product.variants.size > 0 %}
      {{ product.variants.first.price }}
    {% endif %}
    
  4. Default values: Use the default filter for optional values

    {{ product.description | default: "No description" }}
    
  5. Performance: Assign results of expensive operations to variables

    {% assign filtered_products = products | where: 'available', true | sort: 'price' %}
    {% for product in filtered_products %}
      ...
    {% endfor %}
    

Security

Liquid is a language designed to be safe. You cannot:

  • Access the file system directly
  • Execute arbitrary code
  • Modify data (only read and display)
  • Create infinite loops (there are iteration limits)

Server-Side Execution

Remember that all Liquid code is executed on the server before sending HTML to the browser. You cannot use Liquid to react to user events or modify the page after loading.