Liquid Tags
Tags are the programming logic of Liquid templates. They create control flow, loops, and output dynamic content.
{% if user %}
Hello {{ user.name }}!
{% endif %}
OQO extends Liquid, the templating language created by Shopify. All standard Liquid tags work in OQO themes, plus the custom tags documented below.
Collections (Shopify-style)
Posts are accessed via the global collections object using Shopify-style for loops. This is the primary way to display posts in themes.
Basic Usage
{% for post in collections.featured_news %}
<article>
<h3>{{ post.title }}</h3>
<p>{{ post.excerpt }}</p>
<a href="{{ post | post_url }}">Read more</a>
</article>
{% endfor %}
With Limit
{% for post in collections.sports limit: 6 %}
<article>{{ post.title }}</article>
{% endfor %}
Dynamic Collection from Settings
{%- assign collection_slug = section.settings.collection -%}
{% for post in collections[collection_slug] limit: section.settings.count %}
<article>
<img src="{{ post.image | image_url: 'medium' }}" alt="{{ post.image.alt }}">
<h3>{{ post.title }}</h3>
</article>
{% endfor %}
Special Collections
<!-- All approved posts (recent first) -->
{% for post in collections.all_posts limit: 10 %}
{{ post.title }}
{% endfor %}
<!-- All tags -->
{% for tag in collections.all_tags %}
<a href="{{ tag | tag_url }}">{{ tag.name }}</a>
{% endfor %}
Collection Metadata
Access collection properties directly:
{% assign current = collections.sports %}
{% if current %}
<header class="collection-header">
{% if current.image %}
<img src="{{ current.image | image_url: 'hero' }}" alt="{{ current.title }}">
{% endif %}
<h1>{{ current.title }}</h1>
<p>{{ current.description }}</p>
<span>{{ current.posts_count }} {{ current.posts_count | pluralize_word: 'article' }}</span>
</header>
{% endif %}
Collection Properties:
| Property | Type | Description |
|---|---|---|
title | string | Collection name |
handle | string | URL-safe slug |
description | string | Collection description |
image | ImageDrop | Optional hero image |
posts_count | integer | Total posts in collection |
posts | array | Posts in this collection |
post
Resolves and renders a single post. The tag accepts an optional identifier -- a quoted slug, a variable reference, or nothing (to resolve from the current URL).
From URL Parameters (default)
When no argument is provided, the tag resolves the post from the current URL parameters. This is the most common usage for post detail pages:
{% post %}
{% if found %}
<article>
<h1>{{ post.title }}</h1>
{{ post.content_html }}
</article>
{% else %}
<p>Post not found</p>
{% endif %}
{% endpost %}
By Slug (literal string)
Pass a quoted slug to resolve a specific post:
{% post 'welcome-to-our-blog' %}
{% if found %}
<article>
<h1>{{ post.title }}</h1>
{{ post.content_html }}
</article>
{% endif %}
{% endpost %}
From Section Settings (variable)
Pass a variable reference to resolve from a section setting. When the setting contains a PostDrop (from a post setting type), the tag uses it directly. When it contains a slug string, it looks up the post by slug:
{% post section.settings.selected_post %}
{% if found %}
<article>
<h1>{{ post.title }}</h1>
{{ post.content_html }}
</article>
{% endif %}
{% endpost %}
When a variable resolves to nil or blank, the tag falls back to URL parameter resolution. This means a section can work both as a "selected post" display and as a URL-driven post page.
Block Variables
Inside the block, these variables are available:
| Variable | Type | Description |
|---|---|---|
post | PostDrop | The resolved post (nil if not found) |
found | boolean | True if post was resolved successfully |
Complete Example
{% post %}
{% if found %}
<article>
{% if post.image %}
<img src="{{ post.image | image_url: 'hero' }}" alt="{{ post.image.alt }}">
{% endif %}
<h1>{{ post.title }}</h1>
<time>{{ post.published_at | date_format: 'long' }}</time>
{% if post.author %}
<span>By {{ post.author.name }}</span>
{% endif %}
<div class="post-content">
{{ post.content_html }}
</div>
{% unless post.tags.blank? %}
<div class="post-tags">
{% for tag in post.tags %}
<a href="{{ tag | tag_url }}">{{ tag.name }}</a>
{% endfor %}
</div>
{% endunless %}
</article>
{% else %}
<div class="not-found">
<h2>Post not found</h2>
<p>The post you are looking for does not exist.</p>
</div>
{% endif %}
{% endpost %}
section_wrapper
Wraps section content with proper data attributes for the visual editor and live preview.
{% section_wrapper 'section' class: 'my-class' style: section_style data-css-vars: 'bgcolor,overlay' %}
<h2>Content here</h2>
{% endsection_wrapper %}
Output:
<section class="gz-section gz-hero-banner my-class"
data-section-id="abc123"
data-section-type="hero/banner"
data-section-wrapper="true"
data-css-vars="bgcolor,overlay"
style="--bgcolor: #1F2937;">
<h2>Content here</h2>
</section>
Parameters:
| Parameter | Description |
|---|---|
| First argument | HTML tag: 'section', 'div', 'nav', 'header', 'footer', etc. |
class | CSS classes to add |
style | Inline styles (use with CSS variables) |
id | Element ID |
data-css-vars | Comma-separated settings for live CSS variable updates |
data-controller | Stimulus controller(s) to attach |
Any data-* | Custom data attributes |
Auto-Added Classes:
gz-section- Common class for all sectionsgz-{type}- Section-type class (e.g.,gz-hero-banner)
block_wrapper
Wraps block content for editor selection and live preview.
{% for block in section.blocks %}
{% block_wrapper block %}
{% if block.type == 'feature' %}
<div class="feature">
<h3>{{ block.settings.title }}</h3>
<p>{{ block.settings.description }}</p>
</div>
{% endif %}
{% endblock_wrapper %}
{% endfor %}
With custom element and classes:
{% block_wrapper block tag: 'li' class: 'nav-item' %}
<a href="{{ block.settings.link | link_href }}">
{{ block.settings.link | link_text }}
</a>
{% endblock_wrapper %}
For nested blocks (children):
{% for child in block.children %}
{% block_wrapper child source: 'child' %}
{{ child.settings.text }}
{% endblock_wrapper %}
{% endfor %}
content_for_layout
Outputs page sections with optional filtering by section settings. Used in layout files to create multi-column layouts.
Basic Usage
<!-- Output all sections (in layout.liquid) -->
{% content_for_layout %}
Filter by Setting
<!-- Multi-column layout -->
<div class="grid-3col">
<div class="col-left">
{% content_for_layout column: 'left_col' %}
</div>
<div class="col-center">
{% content_for_layout column: 'center_col' %}
</div>
<div class="col-right">
{% content_for_layout column: 'right_col' %}
</div>
</div>
Parameters:
Any key-value pair filters sections by matching section.settings[key] == value.
| Parameter | Type | Description |
|---|---|---|
| (any) | string/boolean | Filter by section setting value |
Section Schema for Column Support:
{
"settings": [
{
"type": "select",
"id": "column",
"label": "Column",
"default": "center_col",
"options": [
{ "value": "left_col", "label": "Left Column" },
{ "value": "center_col", "label": "Center Column" },
{ "value": "right_col", "label": "Right Column" }
]
}
]
}
paginate
Wraps a collection with pagination context.
{% paginate collections.all_posts by 12 %}
{% for post in paginate.collection %}
<article>
<img src="{{ post.image | image_url: 'medium' }}" alt="">
<h3>{{ post.title }}</h3>
</article>
{% endfor %}
<nav class="pagination">
{% if paginate.previous %}
<a href="{{ paginate.previous.url }}">« Previous</a>
{% endif %}
{% for part in paginate.parts %}
{% if part.is_link %}
<a href="{{ part.url }}">{{ part.title }}</a>
{% else %}
<span class="current">{{ part.title }}</span>
{% endif %}
{% endfor %}
{% if paginate.next %}
<a href="{{ paginate.next.url }}">Next »</a>
{% endif %}
</nav>
{% endpaginate %}
Paginate Object:
| Property | Type | Description |
|---|---|---|
page_size | integer | Items per page |
current_page | integer | Current page number |
current_offset | integer | Offset for query |
items | integer | Total items count |
pages | integer | Total pages |
collection | array | Current page's items |
previous | object | Previous page link (or nil) |
next | object | Next page link (or nil) |
parts | array | Page number links |
Part Object:
| Property | Type | Description |
|---|---|---|
title | string | Page number or ellipsis |
url | string | Link URL |
is_link | boolean | True if clickable |
render
Include a snippet (partial template) with local variables.
<!-- Simple include -->
{% render 'card' %}
<!-- With variables -->
{% render 'post-card', post: post, show_excerpt: true %}
<!-- Loop over collection -->
{% render 'item' for items as item %}
<!-- Pass variable with auto-name -->
{% render 'post-card' with post %}
Important: Variables must be passed explicitly - snippets don't inherit parent scope.
Snippet Locations (searched in order):
themes/{theme}/snippets/{name}.liquidthemes/{theme}/partials/{name}.liquidthemes/default/snippets/{name}.liquid(fallback)
Loop Variables (for for syntax):
| Variable | Description |
|---|---|
forloop.index | 1-based index |
forloop.index0 | 0-based index |
forloop.first | True on first iteration |
forloop.last | True on last iteration |
forloop.length | Collection size |
theme_css
Smart CSS loading with live preview support.
<!-- In your layout's <head> -->
{% theme_css %}
Output (Production):
<link rel="stylesheet" href="/themes/default/assets/theme-abc123.css">
Output (Preview):
<link rel="stylesheet" href="/themes/default/assets/theme-abc123.css">
<style id="theme-variables">:root { --theme-primary-color: #ff0000; }</style>
<script>/* Live update listener */</script>
theme_javascript
Smart JavaScript loading with visual editor support.
<!-- At end of layout body -->
{% theme_javascript %}
Outputs the theme's JavaScript plus, in preview mode, the visual editor script for hover overlays and edit buttons.
member_form
Block tag that generates an authentication form with the correct action URL, method, and CSRF token. Handles all the security plumbing so you only need to provide the form fields.
{% member_form "login" %}
<input type="email" name="email" placeholder="Email">
<input type="password" name="password" placeholder="Password">
<button type="submit">Sign In</button>
{% endmember_form %}
Output:
<form action="/member/login" method="post" class="member-form member-form--login">
<input type="hidden" name="authenticity_token" value="...">
<input type="email" name="email" placeholder="Email">
<input type="password" name="password" placeholder="Password">
<button type="submit">Sign In</button>
</form>
Form Types:
| Type | Action URL | Description |
|---|---|---|
login | /member/login | Sign in with email + password |
register | /member/register | Create a new member account |
logout | /member/logout | Sign out the current member |
forgot_password | /member/password/new | Request a password reset email |
reset_password | /member/password/edit | Set a new password (from reset link) |
Block Variables:
Inside the block, a form variable is available:
| Variable | Type | Description |
|---|---|---|
form.errors | string | Error message from the last form submission (if any) |
form.return_to | string | URL to redirect to after authentication (if set) |
Example: Login Form with Error Handling
{% member_form "login" %}
{% if form.errors %}
<div class="form-error">{{ form.errors }}</div>
{% endif %}
<label for="email">Email</label>
<input type="email" name="email" id="email" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<button type="submit">Sign In</button>
<a href="{{ routes.member_password_path }}">Forgot password?</a>
<p>Don't have an account? <a href="{{ routes.member_register_path }}">Register</a></p>
{% endmember_form %}
Example: Registration Form
{% member_form "register" %}
{% if form.errors %}
<div class="form-error">{{ form.errors }}</div>
{% endif %}
<input type="text" name="first_name" placeholder="First Name">
<input type="text" name="last_name" placeholder="Last Name">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Create Account</button>
{% endmember_form %}
Example: Logout Button
{% member_form "logout" %}
<button type="submit">Sign Out</button>
{% endmember_form %}
CSS Classes:
Each form has consistent classes for styling:
.member-form-- Base class for all member forms.member-form--{type}-- Type-specific class (e.g.,.member-form--login,.member-form--register)
member / guest
Conditional block tags that render content based on whether a member is signed in.
{% member %} renders its content only when a member is logged in. {% guest %} renders its content only when no member is logged in. They are complementary tags.
{% member %}
<p>Welcome back, {{ member.display_name }}!</p>
{% member_form "logout" %}
<button type="submit">Sign Out</button>
{% endmember_form %}
{% endmember %}
{% guest %}
<a href="{{ routes.member_login_path }}">Sign In</a>
<a href="{{ routes.member_register_path }}">Create Account</a>
{% endguest %}
Example: Header Auth Navigation
<nav class="auth-nav">
{% member %}
{% if member.avatar_url %}
<img src="{{ member.avatar_url }}" alt="" class="avatar">
{% endif %}
<span>{{ member.display_name }}</span>
{% member_form "logout" %}
<button type="submit" class="btn-link">Sign Out</button>
{% endmember_form %}
{% endmember %}
{% guest %}
<a href="{{ routes.member_login_path }}">Sign In</a>
<a href="{{ routes.member_register_path }}">Join</a>
{% endguest %}
</nav>
These tags are cleaner than using {% if member_logged_in %} conditionals. They read naturally and make templates easier to understand.
login_form
Block tag that renders an OAuth social login form for a specific provider. Handles all security plumbing (POST form, CSRF token, state parameter) while giving theme designers full control over the button appearance.
If the provider is not configured by the site admin, it renders an HTML comment instead.
Basic Usage
{% login_form "google" %}{% endlogin_form %}
{% login_form "github" %}{% endlogin_form %}
When the block body is empty, a default button is rendered with the provider's brand icon and label.
Output (when configured):
<form action="/users/auth/google_oauth2" method="post" class="login-form login-form--google">
<input type="hidden" name="authenticity_token" value="...">
<input type="hidden" name="state" value="...">
<button type="submit" class="login-form-btn login-form-btn--google" data-provider="google">
<svg class="login-form-icon">...</svg>
<span>Continue with Google</span>
</button>
</form>
Output (when NOT configured):
<!-- google login not configured -->
Custom Button
Use the block body to provide your own button markup. A provider variable is available inside the block:
| Variable | Type | Description |
|---|---|---|
provider.name | string | Provider key ("google", "github", etc.) |
provider.label | string | Display name ("Google", "GitHub", etc.) |
provider.icon | string | Brand SVG icon markup |
{% login_form "google" %}
<button type="submit" class="my-social-btn">
{{ provider.icon }}
<span>Sign in with {{ provider.label }}</span>
</button>
{% endlogin_form %}
Member OAuth
Add for: "member" to target member authentication routes instead of the default user routes:
{% login_form "google" for: "member" %}{% endlogin_form %}
This changes the form action from /users/auth/google_oauth2 to /member/auth/google_oauth2.
Available Providers
| Provider | Tag | Display Label |
|---|---|---|
{% login_form "google" %} | Continue with Google | |
{% login_form "facebook" %} | Continue with Facebook | |
| Apple | {% login_form "apple" %} | Continue with Apple |
| Twitter/X | {% login_form "twitter" %} | Continue with X |
| GitHub | {% login_form "github" %} | Continue with GitHub |
{% login_form "linkedin" %} | Continue with LinkedIn |
CSS Classes
Each form and button has consistent classes for styling:
.login-form-- Base class for all login forms.login-form--{provider}-- Provider-specific form class.login-form-btn-- Default button class.login-form-btn--{provider}-- Provider-specific button class.login-form-icon-- SVG icon class
Example: Login Page with Social + Email
<div class="login-page">
<h2>Sign In</h2>
<div class="social-login">
{% login_form "google" for: "member" %}{% endlogin_form %}
{% login_form "github" for: "member" %}{% endlogin_form %}
{% login_form "facebook" for: "member" %}{% endlogin_form %}
</div>
<div class="divider">or</div>
{% member_form "login" %}
{% if form.errors %}
<div class="form-error">{{ form.errors }}</div>
{% endif %}
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Sign In with Email</button>
{% endmember_form %}
</div>
OAuth providers must be configured by the site administrator at Admin > Settings > OAuth before they appear. Unconfigured providers render as HTML comments, so you can safely include all providers in your theme -- only the configured ones will display.
theme_asset
Load CSS, JS, or image assets from the theme.
{% theme_asset 'main.css' %}
{% theme_asset 'theme.js' %}
{% theme_asset 'logo.png' %}
Output:
<link rel="stylesheet" href="/themes/default/assets/main-abc123.css">
<script src="/themes/default/assets/theme-def456.js"></script>
/themes/default/assets/logo.png
Options:
| Option | Description |
|---|---|
type: 'css' | Force CSS output |
type: 'js' | Force JS output |
preload: 'true' | Add preload for CSS |
defer: 'false' | Disable defer for JS |
async: 'true' | Add async for JS |
showcase
Render items in visual layout modes (mondrian, masonry, grid). Useful for displaying collections or tags as visual tiles.
{% showcase layout: 'mondrian' collection: collections.featured columns: 4 %}
<article style="{{ tile.style }}">
<a href="{{ item | collection_url }}"
style="background-image: url('{{ item.image | image_url: 'medium' }}')">
<h3>{{ item.title }}</h3>
</a>
</article>
{% endshowcase %}
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
layout | string | 'mondrian' | 'mondrian', 'masonry', 'grid', 'full_width' |
collection | object | - | Array of items to display |
columns | integer | 4 | Number of columns |
row_height | integer | 160 | Row height in pixels |
height_scale | float | 1.5 | Height multiplier |
overlay_opacity | string | '0.45' | Overlay opacity |
draw_border | boolean | false | Draw borders |
show_description | boolean | false | Show item descriptions |
show_post_count | boolean | false | Show post count |
Block Variables:
| Variable | Type | Description |
|---|---|---|
item | Drop | Current item (CollectionDrop, TagDrop, or PostDrop) |
tile | TileDrop | Tile info (style, width, height, variant) |
forloop | Hash | Standard loop object |
schema
Define section settings. Must be at the end of the section file.
{% schema %}
{
"name": "Hero Banner",
"category": "hero",
"settings": [
{
"type": "text",
"id": "title",
"label": "Title",
"default": "Welcome"
},
{
"type": "image",
"id": "background_image",
"label": "Background Image"
},
{
"type": "checkbox",
"id": "show_overlay",
"label": "Show Overlay",
"default": true
}
],
"blocks": [
{
"type": "button",
"name": "Button",
"settings": [
{
"type": "link",
"id": "link",
"label": "Button Link"
}
]
}
]
}
{% endschema %}
See Settings System for full schema documentation.
style
Defines per-section CSS that gets extracted, compiled, and served as a separate CSS file. This is the Shopify-style approach where section CSS lives alongside the section template in the same .liquid file.
The {% style %} block does not render inline in the HTML output. Instead, OQO's CSS compiler extracts these blocks and pre-compiles them into separate CSS files that are loaded via {% theme_css %}.
Basic Usage
{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
--section-bgcolor: {{ section.settings.bgcolor }};
background-color: var(--section-bgcolor);
}
{% endstyle %}
With Conditional Settings
Use Liquid conditionals inside {% style %} blocks to apply CSS variables only when a setting has a value:
{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
{% if section.settings.custom_colors and section.settings.bgcolor != blank %}
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
{% else %}
--section-bgcolor: var(--theme-background-color);
{% endif %}
{% if section.settings.custom_colors and section.settings.text_color != blank %}
--section-text-color: {{ section.settings.text_color | color_alpha_to_rgba }};
{% else %}
--section-text-color: var(--theme-text-color);
{% endif %}
background-color: var(--section-bgcolor);
color: var(--section-text-color);
}
{% endstyle %}
How It Works
- You write a
{% style %}...{% endstyle %}block in your section's.liquidfile - The CSS compiler extracts the block and processes it as a Liquid template with
section.idandsection.settingsavailable - The compiled CSS is written to a separate file and loaded automatically
- In preview mode, the block is re-rendered on the fly when settings change, enabling live hot-reload of CSS variables
Rules
| Rule | Why |
|---|---|
Use [data-section-wrapper][data-section-id="{{ section.id }}"] as the selector | Scopes CSS to the specific section instance |
| Stick to CSS variables and simple property rules | Blocks are re-rendered on every settings change and must be lightweight |
Use --section-* prefix for your CSS variables | Distinguishes section-level variables from --theme-* global variables |
| Put animations, hover effects, and complex CSS in the theme stylesheet | Static styles do not need hot-reload and belong in the main CSS |
Complete Section Example
A section template with HTML, a {% style %} block, and a {% schema %}:
{% section_wrapper 'section' class: 'hero-banner' %}
<div class="hero-content">
<h1>{{ section.settings.title }}</h1>
<p>{{ section.settings.subtitle }}</p>
</div>
{% endsection_wrapper %}
{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
{% if section.settings.bgcolor != blank %}
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
{% else %}
--section-bgcolor: var(--theme-background-color);
{% endif %}
background-color: var(--section-bgcolor);
min-height: 400px;
display: flex;
align-items: center;
}
{% endstyle %}
{% schema %}
{
"name": "Hero Banner",
"settings": [
{ "type": "text", "id": "title", "label": "Title" },
{ "type": "text", "id": "subtitle", "label": "Subtitle" },
{ "type": "color_alpha", "id": "bgcolor", "label": "Background Color" }
]
}
{% endschema %}
zone
Declares a layout zone where sections can be placed. Zones are the building blocks of layout templates -- they define the regions (header, footer, sidebar, etc.) where site-wide sections live.
The admin UI automatically discovers zones by parsing your layout template. No configuration needed beyond adding the tag.
Basic Usage
{% zone 'header' %}{% endzone %}
<main>
{% content_for_layout %}
</main>
{% zone 'footer' %}{% endzone %}
With Default Content
Default content is shown when no sections have been assigned to the zone:
{% zone 'sidebar' %}
<aside class="default-sidebar">
<p>Add sections to customize this sidebar</p>
</aside>
{% endzone %}
With Wrapper Options
When you provide wrapper options, the zone's content is wrapped in an HTML element:
{% zone 'header' tag: 'header' class: 'site-header' %}{% endzone %}
{% zone 'footer' tag: 'footer' id: 'site-footer' %}{% endzone %}
{% zone 'sidebar' tag: 'aside' class: 'sidebar-area' %}{% endzone %}
Without options, section HTML is output directly with no wrapper element.
Parameters
| Parameter | Type | Description |
|---|---|---|
| First argument | string | Zone name (required) |
tag | string | Wrapper HTML element: div, header, footer, aside, nav, section, main, article |
class | string | CSS classes for wrapper |
id | string | HTML id for wrapper |
Zone Naming
Use any name you want. Names automatically become human-readable labels in the admin UI:
| Zone Name | Admin Label |
|---|---|
header | Header |
left_sidebar | Left Sidebar |
above_fold | Above Fold |
main_navigation | Main Navigation |
Complete Layout Example
<!DOCTYPE html>
<html lang="{{ request.locale | default: 'en' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page.title | default: settings.site_name }}</title>
{% theme_css %}
</head>
<body class="gz-theme">
{% zone 'header' tag: 'header' %}{% endzone %}
<main class="gz-layout">
{% content_for_layout %}
</main>
{% zone 'footer' tag: 'footer' %}{% endzone %}
{% theme_javascript %}
</body>
</html>
Multi-Zone Newspaper Layout
{% zone 'topbar' %}{% endzone %}
{% zone 'masthead' %}{% endzone %}
{% zone 'navigation' tag: 'nav' %}{% endzone %}
<main class="np-container">
{% zone 'above_fold' %}{% endzone %}
<div class="np-grid-3col">
<aside class="np-col np-col--left">
{% zone 'left_sidebar' %}{% endzone %}
</aside>
<article class="np-col np-col--main">
{% content_for_layout %}
</article>
<aside class="np-col np-col--right">
{% zone 'right_sidebar' %}{% endzone %}
</aside>
</div>
{% zone 'below_fold' %}{% endzone %}
</main>
{% zone 'footer' tag: 'footer' %}{% endzone %}
Zone sections are site-wide -- they appear on every page that uses the layout. They are rendered by {% zone %} tags. Page sections are specific to a single page and are rendered by {% content_for_layout %}.
extends
Enables layout inheritance. Child layouts extend a parent layout and override specific zones while inheriting everything else. This keeps your layouts DRY -- define the HTML boilerplate once in a base layout, then create variants by overriding only the zones that differ.
Basic Inheritance
{%- comment -%} layouts/base.liquid -- parent layout {%- endcomment -%}
<!DOCTYPE html>
<html>
<head>{% theme_css %}</head>
<body>
{% zone 'header' %}<header>Default Header</header>{% endzone %}
<main>
{% zone 'main' %}{% content_for_layout %}{% endzone %}
</main>
{% zone 'footer' %}<footer>Default Footer</footer>{% endzone %}
{% theme_javascript %}
</body>
</html>
{%- comment -%} layouts/landing.liquid -- child layout {%- endcomment -%}
{% extends 'base' %}
{% zone 'header' %}
<header class="landing-header">
<h1>Landing Page</h1>
</header>
{% endzone %}
{%- comment -%} main and footer zones are inherited from base {%- endcomment -%}
{% endextends %}
How It Works
- The child declares
{% extends 'parent_name' %}at the top - Inside the extends block, the child redefines only the zones it wants to override
- The block ends with
{% endextends %} - Zones not redefined in the child inherit the parent's content
- The parent's full HTML structure wraps everything
Parameters
| Parameter | Type | Description |
|---|---|---|
| First argument | string | Parent layout name (required, matches filename without .liquid) |
Using zone.super
Include the parent's zone content within an override using {{ zone.super }}:
{% extends 'base' %}
{% zone 'header' %}
<div class="promo-banner">Limited Time Offer!</div>
{{ zone.super }}
{% endzone %}
{% endextends %}
This renders the promo banner above the parent's default header content.
Multi-Level Inheritance
Layouts can chain multiple levels. For example, article.liquid extends default.liquid, which extends base.liquid:
{%- comment -%} layouts/base.liquid {%- endcomment -%}
<!DOCTYPE html>
<html>
<head>{% theme_css %}</head>
<body>
{% zone 'header' %}{% endzone %}
{% zone 'main' %}{% content_for_layout %}{% endzone %}
{% zone 'footer' %}{% endzone %}
{% theme_javascript %}
</body>
</html>
{%- comment -%} layouts/default.liquid -- extends base {%- endcomment -%}
{% extends 'base' %}
{% zone 'header' %}
{% zone 'topbar' %}{% endzone %}
{% zone 'masthead' %}{% endzone %}
{% zone 'navigation' %}{% endzone %}
{% endzone %}
{% endextends %}
{%- comment -%} layouts/article.liquid -- extends default {%- endcomment -%}
{% extends 'default' %}
{% zone 'main' %}
<article class="article-layout">
{% zone 'article_header' %}{% endzone %}
{% content_for_layout %}
{% zone 'article_footer' %}{% endzone %}
</article>
{% endzone %}
{% endextends %}
Example Use Cases
| Layout Variant | Strategy |
|---|---|
| Landing page | Extends default, overrides header with minimal branding |
| Article layout | Extends default, adds article_header and article_footer zones |
| Minimal layout | Extends base, overrides header with just a logo |
| Print layout | Extends base, removes all navigation zones |
Standard Liquid Tags
These standard Liquid tags are commonly used in OQO themes:
Control Flow
{% if section.settings.show_title %}
<h2>{{ section.settings.title }}</h2>
{% endif %}
{% if post.image.present? %}
<img src="{{ post.image | image_url: 'medium' }}">
{% elsif section.settings.fallback_image %}
<img src="{{ section.settings.fallback_image | image_url: 'medium' }}">
{% else %}
<div class="placeholder"></div>
{% endif %}
{% unless post.tags.blank? %}
{% for tag in post.tags %}
<a href="{{ tag | tag_url }}">{{ tag.name }}</a>
{% endfor %}
{% endunless %}
{% case section.settings.columns %}
{% when '2' %}
<div class="grid-cols-2">
{% when '3' %}
<div class="grid-cols-3">
{% else %}
<div class="grid-cols-1">
{% endcase %}
Loops
{% for block in section.blocks %}
{% block_wrapper block %}
<div>{{ block.settings.title }}</div>
{% endblock_wrapper %}
{% endfor %}
{% for post in collections.featured limit: 3 offset: 1 %}
<article>{{ post.title }}</article>
{% endfor %}
{% for tag in post.tags %}
<a href="{{ tag | tag_url }}">{{ tag.name }}</a>
{% unless forloop.last %}, {% endunless %}
{% endfor %}
Variables
{% assign card_width = section.settings.card_width | default: 'w-72' %}
{% capture section_style %}
--bgcolor: {{ section.settings.bgcolor }};
--overlay: {{ section.settings.overlay | color_alpha_to_rgba }};
{% endcapture %}
Comments
{%- comment -%}
This is a multi-line comment.
It won't appear in the output.
{%- endcomment -%}
Whitespace Control
Use - to strip whitespace before and after tags:
{%- if condition -%}
content
{%- endif -%}
Without whitespace control:
content
With whitespace control:
content
Related Documentation
- Liquid Objects - Data objects like
member,flash,routes,post,collection,section - Liquid Filters - Filters like
image_url,post_url, and navigation filters (next_post,prev_post) - Settings System - Defining section and theme settings
- Theme Structure - Theme directory layout and file conventions