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
productobject is available
product.sku
The product’s SKU (Stock Keeping Unit) at the product level.
{{ product.sku }}Example Output:
GLK-G19-GEN5-BLKNote: 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:
764503913150Note: 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 onLoop 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 valuetemplate.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 localepowered_by_link
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 summaryNotes:
- Prices are integers in cents. Always pipe them through
money— a bare{{ product.price }}prints59999. manufacturer,brandandvendorare empty strings when unset, so test them with!= blankrather than for truthiness.- There is no
product.tags— useproduct.product_type,product.manufactureror 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 descriptionPolicy 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 namesarticle.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.
search
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.99Date Formatting
{{ article.published_at | date: '%B %d, %Y' }}
# Output: January 15, 2025
{{ article.published_at | date: '%m/%d/%Y' }}
# Output: 01/15/2025String 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 emptyImage 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 settingimage_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 spriteOnly pass filenames that actually exist in the theme. An unknown filename resolves to a same-domain URL that 404s silently, with nothing reported anywhere.
Link Filters
{{ 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 variableWhen 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.
Related Documentation
- Visual Editor - Theme customization
- Theme Sections - Section management
- Creating Products - Product specifications

