Skip to content

Liquid Variables

Custom Liquid variables available in FirearmCart themes

Updated View as Markdown

FirearmCart themes use Liquid templating. In addition to standard Liquid objects, FirearmCart provides custom variables tailored for firearms and ammunition retailers.


Overview

What is Liquid?

Liquid is a template language that allows you to:

  • Display dynamic content
  • Access product, collection, and store data
  • Create logic and loops
  • Use filters to transform data

Liquid Syntax

Objects - Output data:

{{ product.title }}
{{ shop.name }}

Tags - Logic and control:

{% if product.available %}
  In Stock
{% endif %}

Filters - Transform output:

{{ product.price | money }}
{{ product.title | upcase }}

Where Theme Files Live

Themes follow the conventional folder layout:

Path Contents
layout/theme.liquid The base layout wrapping every page
templates/ Page templates, usually JSON
sections/ Sections, each with its own settings schema
snippets/ Partials pulled in with {% render %}
blocks/ Theme blocks
assets/ CSS, JavaScript and images
config/ Theme settings schema and saved values
locales/ Translation files used by the t filter

FirearmCart Product Variables

product.specifications

Outputs an HTML table of product specifications. This is useful for displaying firearm details like caliber, barrel length, capacity, and other technical specifications.

Usage:

{{ product.specifications }}

Output Example:

For a product with specifications:

Name Value
Category Handguns
Model 1911 Commander
Caliber .45 ACP
Barrel Length 4.25“
State Restrictions CA

The variable outputs:

<table class="product-specifications">
  <tbody>
    <tr><th>Category</th><td>Handguns</td></tr>
    <tr><th>Model</th><td>1911 Commander</td></tr>
    <tr><th>Caliber</th><td>.45 ACP</td></tr>
    <tr><th>Barrel Length</th><td>4.25"</td></tr>
    <tr><th>State Restrictions</th><td>CA</td></tr>
  </tbody>
</table>

Styling:

Style the specifications table using the .product-specifications CSS class:

.product-specifications {
  width: 100%;
  border-collapse: collapse;
}

.product-specifications th,
.product-specifications td {
  padding: 8px 12px;
  border-bottom: 1px solid #e5e5e5;
  text-align: left;
}

.product-specifications th {
  font-weight: 600;
  width: 40%;
}

Notes:

  • Returns an empty string if no specifications are set
  • All values are HTML-escaped for security
  • Can be used in any template where the product object is available

product.sku

The product’s SKU (Stock Keeping Unit) at the product level.

{{ product.sku }}

Example Output:

GLK-G19-GEN5-BLK

Note: For products with variants, each variant also has its own SKU accessible via product.selected_or_first_available_variant.sku.


product.barcode

The product’s barcode (UPC, EAN, ISBN, etc.) at the product level.

{{ product.barcode }}

Example Output:

764503913150

Note: For products with variants, each variant also has its own barcode accessible via the variant object.


product.rating

Outputs an inline SVG star rating based on product reviews — five stars, filled to the average, with half-stars supported.

{{ product.rating }}

Output Example:

For a product with a 4.2 rating:

<span class="product-rating__stars" style="display:inline-flex;gap:1px;align-items:center">
  <svg ...></svg>  <!-- one per star -->
</span>

Important Notes:

  • Returns an empty string if the product has no reviews
  • This is HTML, not a number — echo it, never compare it to a value
  • The stars carry inline styles, including their colour, so a CSS rule on the wrapper will not restyle them
  • Output is inline SVG, not text characters
  • Can be used in custom liquid blocks

product.rating_full

Outputs stars plus a text summary showing the average rating and review count.

{{ product.rating_full }}

Output Example:

For a product with 37 reviews and a 4.2 average rating:

<span class="product-rating" style="display:inline-flex;align-items:center;gap:0.4em">
  <span class="product-rating__stars">…</span>
  <span class="product-rating__text" style="font-size:0.9em;color:#6b7280">4.2 (37 reviews)</span>
</span>

Important Notes:

  • Returns an empty string unless the product has at least one review
  • Includes both visual stars and text summary
  • The review count is pluralized for you (“1 review” / “37 reviews”)
  • Can be used in custom liquid blocks

Using Product Ratings in Custom Liquid Blocks

Product rating variables are commonly used in custom liquid blocks within theme sections or custom code:

Basic Usage:

{% comment %} Just display stars {% endcomment %}
{{ product.rating }}

{% comment %} Display stars with text summary {% endcomment %}
{{ product.rating_full }}

Conditional Display:

Only show ratings if reviews exist:

{% if product.rating != blank %}
  <div class="product-reviews">
    {{ product.rating_full }}
  </div>
{% endif %}

Always guard with != blank. An unreviewed product returns an empty string, and an empty string is truthy in Liquid — so a bare {% if product.rating %} is always true.

In Product Cards:

<div class="product-card">
  <h3>{{ product.title }}</h3>
  <p>{{ product.price | money }}</p>

  {% if product.rating != blank %}
    {{ product.rating }}
  {% endif %}

  <a href="{{ product.url }}">View Product</a>
</div>

Styling:

The wrappers are yours to style, but the stars themselves are drawn with inline styles and will ignore your colours:

.product-rating {
  vertical-align: middle;
}

.product-rating__text {
  font-size: 14px;
  color: #666;
}

Review counts: the numeric aggregates behind these variables are also available, when you want the values rather than the rendered stars:

{{ product.metafields.reviews.rating.value.rating }}   {% comment %} e.g. 4.2 {% endcomment %}
{{ product.metafields.reviews.rating_count }}          {% comment %} e.g. 37  {% endcomment %}

Global Objects

shop

Store information available on every page:

{{ shop.name }}                      # Store name
{{ shop.currency }}                  # Currency code (USD)
{{ shop.products_count }}            # Number of storefront-visible products
{{ shop.logo }}                      # Store logo image object
{{ shop.brand.logo }}                # Same logo, under the brand object
{{ shop.policies }}                  # Published store policies
{{ shop.enabled_payment_types }}     # Payment badges to display
{{ shop.customer_accounts_enabled }} # Whether customer accounts are on

Loop shop.policies for the footer rather than hardcoding policy links — each entry has title, url, handle and body, and they are served at /policies/{handle}. An unpublished policy disappears from this list and its URL returns 404.

shop.email, shop.phone and shop.domain exist for theme compatibility but are always empty — a theme that prints them shows nothing. Put contact details in a page or a section setting instead.


settings

Theme settings defined in the theme’s settings schema:

{{ settings.logo }}
{{ settings.logo_height }}
{{ settings.color_schemes }}

The exact setting names come from your theme. Three are supplied by the platform regardless of theme: settings.logo (taken from your store settings and overriding whatever the theme stored), plus settings.logo_height and settings.logo_height_mobile.

Theme-Level vs Section-Level:

{% comment %} Theme settings (global) {% endcomment %}
{{ settings.logo }}

{% comment %} Section settings (local to section) {% endcomment %}
{{ section.settings.heading }}

cart

Shopping cart information:

{{ cart.item_count }}            # Number of items
{{ cart.total_price | money }}   # Cart total (in cents — pipe through money)
{{ cart.original_total_price | money }}
{{ cart.total_discount | money }}
{{ cart.items }}                 # Array of cart items
{{ cart.currency.symbol }}       # $

{% for item in cart.items %}
  {{ item.product.title }} - {{ item.quantity }}
  {{ item.final_line_price | money }}
{% endfor %}

template

Current template being rendered:

{{ template }}               # Template name (e.g., "product", "collection")
{{ template.name }}          # Same value

template.suffix and template.directory exist for compatibility but are always empty.


routes

Route helpers for generating URLs:

{{ routes.root_url }}                    # Home page
{{ routes.search_url }}                  # Search page
{{ routes.predictive_search_url }}       # Search suggestions endpoint
{{ routes.all_products_collection_url }} # All products
{{ routes.cart_url }}                    # Cart page
{{ routes.cart_add_url }}                # Cart endpoints, for theme JavaScript
{{ routes.cart_change_url }}
{{ routes.cart_update_url }}
{{ routes.cart_clear_url }}
{{ routes.account_url }}                 # Account page
{{ routes.account_login_url }}           # Login page
{{ routes.account_logout_url }}
{{ routes.account_register_url }}
{{ routes.account_orders_url }}
{{ routes.account_addresses_url }}
{{ routes.product_recommendations_url }}

Always use these rather than hardcoding paths — in preview and editor contexts they carry a prefix that a literal /cart would miss.


request

{{ request.design_mode }}    # true while rendering inside the editor
{{ request.locale }}         # Current locale

Outputs the complete “Powered by FirearmCart” link as HTML. A custom footer must emit it:

{{ powered_by_link }}

social

Your store’s social profile URLs, for footer and header icons:

{{ social.facebook }}
{{ social.instagram }}
{{ social.youtube }}
{{ social.twitter }}
{{ social.tiktok }}
{{ social.pinterest }}

linklists

Your storefront navigation menus, keyed by handle. main-menu always resolves to the active menu:

{% for link in linklists['main-menu'].links %}
  <a href="{{ link.url }}" {% if link.current %}aria-current="page"{% endif %}>{{ link.title }}</a>
{% endfor %}

Each link has title, url, handle, current, active, and nested links for submenus. menus is an alias for the same data.

A link_list section setting is resolved to the menu object for you, so use section.settings.menu.links directly — do not look the value up in linklists.


Page-Specific Objects

product

Available on product pages:

{{ product.title }}          # Product name
{{ product.handle }}         # URL slug
{{ product.url }}            # Product URL
{{ product.description }}    # Full description
{{ product.price | money }}  # Current price (stored in cents)
{{ product.compare_at_price | money }} # Original price, only when above the selling price
{{ product.on_sale }}        # true when compare-at price is above the selling price
{{ product.price_min | money }}
{{ product.price_max | money }}
{{ product.price_varies }}   # true when variants differ in price
{{ product.available }}      # In stock?
{{ product.product_type }}   # Product type
{{ product.vendor }}         # Manufacturer, falling back to brand
{{ product.featured_image }} # First image
{{ product.images }}         # Product images
{{ product.media }}          # Product media
{{ product.variants }}       # Product variants
{{ product.options_with_values }} # Options with their values
{{ product.has_only_default_variant }}
{{ product.selected_or_first_available_variant }} # Current variant

{% comment %} FirearmCart specific {% endcomment %}
{{ product.specifications }} # Specifications table
{{ product.sku }}            # Product SKU
{{ product.barcode }}        # Product barcode
{{ product.manufacturer }}   # Manufacturer
{{ product.brand }}          # Brand
{{ product.rating }}         # Star rating (SVG)
{{ product.rating_full }}    # Stars + text summary

Notes:

  • Prices are integers in cents. Always pipe them through money — a bare {{ product.price }} prints 59999.
  • manufacturer, brand and vendor are empty strings when unset, so test them with != blank rather than for truthiness.
  • There is no product.tags — use product.product_type, product.manufacturer or a collection to group products.

collection

Available on collection pages:

{{ collection.title }}       # Collection name
{{ collection.handle }}      # URL slug
{{ collection.description }} # Collection description
{{ collection.products }}    # Products on the current page of the collection
{{ collection.products_count }} # Number of products
{{ collection.image }}       # Collection image
{{ collection.url }}         # Collection URL
{{ collection.filters }}     # Available filter groups
{{ collection.sort_options }} # Available sort orders

{% for product in collection.products %}
  {{ product.title }}
{% endfor %}

collection.products holds one page of results, not the whole collection.

collection.filters lists the filter groups for the current page in Shopify’s shape. Each group has a label, param_name, type and values, and each value carries label, count, active, url_to_add and url_to_remove. The groups are Availability (filter.v.availability), Price (filter.v.price, a price_range with min_value and max_value), Sale (filter.p.on_sale), In Store (filter.p.in_store) when the catalog uses it, Product type (filter.p.product_type) when the page spans more than one type or one is selected, and specification groups (filter.p.m.custom.*) limited by the theme’s Collection Filters setting. Specification groups are sorted alphabetically.


page

Available on pages:

{{ page.title }}             # Page title
{{ page.content }}           # Page content (HTML)
{{ page.handle }}            # Page handle/slug
{{ page.description }}       # Page description
{{ page.seo_title }}         # SEO title
{{ page.seo_description }}   # SEO description

Policy pages (/policies/{handle}) render through the page object too, with the policy’s name as page.title and its text as page.content.


article

Available on blog article pages:

{{ article.title }}          # Article title
{{ article.handle }}         # URL slug
{{ article.url }}            # Article URL
{{ article.content }}        # Article content (HTML)
{{ article.excerpt }}        # Excerpt, or the first 200 characters of the content
{{ article.author }}         # Author name
{{ article.published_at }}   # Publish date
{{ article.published_at | date: '%B %d, %Y' }}
{{ article.image }}          # Featured image, or nil when there is none
{{ article.tags }}           # Article tag names

article.published_at is already a readable date string, so printing it bare works; pipe it through date when you want a different format. article.published_at_timestamp holds the same moment as a Unix timestamp.

article.tags holds tag names, while tag URLs use the slugified handle. To link a tag to its filtered page, use link_to_type, which builds /blogs/{blog}/tagged/{tag} and slugifies the name for you:

{% for tag in article.tags %}
  {{ tag | link_to_type: blog.handle }}
{% endfor %}

blog

Available on blog pages:

{{ blog.title }}             # Blog title
{{ blog.handle }}            # URL slug
{{ blog.url }}               # Blog URL
{{ blog.articles }}          # Published articles, newest first
{{ blog.articles_count }}    # Number of published articles
{{ blog.all_tags }}          # Every tag used in this blog

{% for article in blog.articles %}
  {{ article.title }}
{% endfor %}

Every blog is also reachable from any page through the global blogs object, keyed by handle:

{% assign news = blogs['news'] %}
{% for article in news.articles limit: 3 %}
  <a href="{{ article.url }}">{{ article.title }}</a>
{% endfor %}

There is no global articles object — articles are reached through a blog, or as the article object on an article page. Note that a blog section setting is already resolved to the blog object, so use section.settings.blog directly rather than looking it up in blogs.


Available on search results pages:

{{ search.terms }}           # Search query
{{ search.performed }}       # true once a query has been submitted
{{ search.results }}         # Search results
{{ search.results_count }}   # Number of results

{% for result in search.results %}
  {{ result.title }}
{% endfor %}

Section Objects

section

Available within sections:

{{ section.id }}             # Unique section ID
{{ section.type }}           # Section type (its file name)
{{ section.settings.heading }} # Section setting value
{{ section.blocks }}         # Section blocks
{{ section.block_order }}    # Block IDs in display order

{% for block in section.blocks %}
  {{ block.id }}             # Block ID
  {{ block.type }}           # Block type
  {{ block.settings.text }}  # Block setting
{% endfor %}

Important: Use section.settings.* for anything the section declares in its own schema. Bare settings.* is the theme-global scope — correct for values you did not declare, such as settings.logo.


Common Filters

Money Formatting

Prices are stored in cents. Every money filter divides by 100 for you:

{{ product.price | money }}
# Output: $599.99

{{ product.price | money_without_currency }}
# Output: 599.99

{{ product.price | money_without_trailing_zeros }}
# Output: $599  (or $599.50 when there are cents)

{{ product.price | money_with_currency }}
# Output: $599.99

Date Formatting

{{ article.published_at | date: '%B %d, %Y' }}
# Output: January 15, 2025

{{ article.published_at | date: '%m/%d/%Y' }}
# Output: 01/15/2025

String Filters

{{ product.title | upcase }}
# Output: GLOCK 19 GEN 5

{{ product.title | downcase }}
# Output: glock 19 gen 5

{{ product.description | truncate: 100 }}
# Truncates to 100 characters

{{ product.description | strip_html }}
# Removes HTML tags

{{ product.handle | replace: '-', ' ' }}
# Replaces hyphens with spaces

{{ product.title | escape }}
# HTML-escapes the value — output is never escaped for you

{{ section.settings.heading | default: 'Featured Products' }}
# Falls back when the value is empty

Image Filters

{{ product.featured_image | image_url: width: 400 }}
# Image URL at 400px width

{{ product.featured_image | image_url: width: 400, height: 400 }}
# Image URL at specific dimensions

{{ section.settings.image | image_url: width: 1200, format: 'webp' }}
# Requesting a format

{{ section.settings.image | image_url: width: 1200 | image_tag: alt: shop.name }}
# Complete <img> tag — always pipe through image_url first

{{ 'hero-1' | placeholder_svg_tag: 'my-section__placeholder' }}
# Designed placeholder for an empty image setting

image_url accepts width, height and format. Always pass a width — with no arguments it returns the original image, and the browser downloads the full-size file. A crop argument is accepted for compatibility but ignored, so do your cropping in CSS with object-fit.

Never pipe an SVG through image_url — use asset_url on its own.


Asset Filters

{{ 'base.css' | asset_url }}
# Theme asset URL, with a cache-busting version

{{ 'base.css' | asset_url | stylesheet_tag }}
# Complete <link> tag

{{ 'theme.js' | asset_url | script_tag }}
# Complete <script> tag

{{ 'icon-sprite.svg' | inline_asset_content }}
# Inlines a text asset, such as an SVG sprite

Only pass filenames that actually exist in the theme. An unknown filename resolves to a same-domain URL that 404s silently, with nothing reported anywhere.


{{ shop.name | link_to: routes.root_url }}
# <a href="/">Store name</a>

{{ tag | link_to_type: blog.handle }}
# <a href="/blogs/news/tagged/announcements">Announcements</a>

For ordinary links, write the anchor yourself and use the routes object for the URL.


Translation Filter

{{ 'products.add_to_cart' | t }}
# Translated string from the theme's locale files

{{ 'products.items_count' | t: count: cart.item_count }}
# Translated with variable

When a key has no translation, the key itself is output — that string appearing on your storefront means the locale file is missing the entry.


Control Flow Tags

Conditionals

{% if product.available %}
  <button>Add to Cart</button>
{% else %}
  <button disabled>Out of Stock</button>
{% endif %}

{% if product.product_type == 'Firearm' %}
  <p>FFL Transfer Required</p>
{% elsif product.product_type == 'Ammunition' %}
  <p>Ground Shipping Only</p>
{% endif %}

{% unless product.available %}
  <span>Sold Out</span>
{% endunless %}

Loops

{% for product in collection.products %}
  <div class="product-card">
    {{ product.title }}
  </div>
{% endfor %}

{% for product in collection.products limit: 4 %}
  {% comment %} Show only first 4 {% endcomment %}
{% endfor %}

{% for product in collection.products offset: 4 %}
  {% comment %} Skip first 4 {% endcomment %}
{% endfor %}

Loop Variables:

{% for product in collection.products %}
  {{ forloop.index }}      # 1, 2, 3...
  {{ forloop.index0 }}     # 0, 1, 2...
  {{ forloop.first }}      # true for first item
  {{ forloop.last }}       # true for last item
  {{ forloop.length }}     # total count
{% endfor %}

Case/When

{% case product.product_type %}
  {% when 'Firearm' %}
    <p>Requires FFL Transfer</p>
  {% when 'Ammunition' %}
    <p>Ships Ground Only</p>
  {% else %}
    <p>Standard Shipping</p>
{% endcase %}

Variable Tags

assign

{% assign featured_product = collection.products.first %}
{{ featured_product.title }}

capture

{% capture product_info %}
  {{ product.title }} - {{ product.price | money }}
{% endcapture %}

{{ product_info }}

Including Templates

render

Include a snippet:

{% render 'product-card', product: product %}

With parameters:

{% render 'icon', icon: 'cart', size: 24 %}

section

Render a section file directly:

{% section 'featured-products' %}

Pages are normally composed from JSON page templates and section groups instead, which is what makes sections reorderable in the Visual Editor. A theme whose base layout pulls in individual sections with this tag will fail import — see Theme Sections.


comment

{% comment %} Notes for other developers — never rendered {% endcomment %}

Liquid has no {# ... #} comment syntax; anything written that way is printed to the page as text.


Best Practices

Check for Empty Values

{% if product.description != blank %}
  {{ product.description }}
{% endif %}

Use Default Values

{{ section.settings.heading | default: 'Featured Products' }}

Optimize Loops

{% comment %} Limit loops for performance {% endcomment %}
{% for product in collection.products limit: 12 %}
  ...
{% endfor %}

HTML Escape

Output is not escaped for you. Escape any value that a customer or merchant typed and that is meant to be plain text:

{% comment %} Escaped — safe in an attribute or as text {% endcomment %}
<img alt="{{ product.title | escape }}">

{% comment %} Raw HTML — correct for rich text fields, which are meant to contain markup {% endcomment %}
{{ product.description }}
{{ product.specifications }}

Unknown Filters Fail Silently

A misspelled or non-existent filter is not an error — the value passes through unchanged. {{ product.title | uppcase }} renders the plain title, and nothing reports a problem, so check spelling when a filter appears to have no effect.


Navigation

Type to search…

↑↓ navigate↵ selectEsc close