This is the page we keep open while working on Shopify stores. It covers the Liquid that themes are written in, where each theme file lives, the CLI commands, and the store settings we check before any launch. It is written for Online Store 2.0 themes, which is every theme in the Shopify Theme Store today, and it was last checked against Shopify's own documentation in September 2026.
If you only need one thing from it: prices are stored in cents, loops stop at 50, and images need a width.
Liquid in one minute
Liquid is Shopify's template language. It runs on Shopify's servers, reads your store's data, and outputs the HTML the customer receives. There are three pieces of syntax to learn, and everything else is vocabulary.
| Syntax | What it does | Example |
|---|---|---|
{{ … }} | Outputs a value | {{ product.title }} |
{% … %} | Runs logic, outputs nothing itself | {% if product.available %} |
| | Passes a value through a filter | {{ product.price | money }} |
{%- … -%} | Same as above, with surrounding whitespace trimmed | {%- if cart.item_count > 0 -%} |
{% if product.available %}
<h1>{{ product.title }}</h1>
<p>{{ product.price | money }}</p>
{% else %}
<p>Sold out</p>
{% endif %}
Two rules catch everyone once. Conditions with and and or are evaluated right to left, and Liquid has no parentheses to change that, so a complicated condition is better written as nested if tags. And contains works on strings and on arrays of strings, not on arrays of objects.
Objects you will use every day
An object is data Shopify hands to your theme. Which objects exist depends on the page: product exists on a product page, collection on a collection page, and a few are available everywhere.
| Object | Where | Properties you will reach for |
|---|---|---|
product | Product pages, and anywhere you loop over products | title, handle, url, price, compare_at_price, available, vendor, type, tags, variants, selected_or_first_available_variant, featured_media, media, metafields |
variant | Inside product.variants | id, title, price, sku, available, inventory_quantity, option1–option3 |
collection | Collection pages | title, handle, url, description, products, products_count, all_products_count, filters, sort_by |
cart | Everywhere | item_count, total_price, items, note |
customer | Everywhere, empty when nobody is logged in | first_name, email, tags, orders_count |
shop | Everywhere | name, url, currency, email |
request | Everywhere | page_type, path, locale, design_mode (true inside the theme editor) |
routes | Everywhere | root_url, cart_url, account_url, search_url, collections_url |
settings | Everywhere | Every theme setting defined in config/settings_schema.json |
section, block | Inside sections and blocks | section.settings, section.id, block.settings, block.type, block.shopify_attributes |
You can also reach specific items from any page by their handle, the lowercase, hyphenated version of the title that also appears in the URL. A product called "Blue T-Shirt" has the handle blue-t-shirt.
| Lookup | Returns | Watch out for |
|---|---|---|
all_products['blue-t-shirt'] | One product | Limited to 20 unique handles per page. More than that, use a collection |
collections['summer'] | One collection | Its products still stop at 50 without pagination |
pages['about-us'] | One page | Returns empty, not an error, if the handle is wrong |
linklists['main-menu'].links | A navigation menu's links | The handle comes from the menu, not its title |
metaobjects.testimonials.values | Every entry of a metaobject type | See the metafields section below |
Tags
Tags are the logic. Every block tag has an end tag: if and endif, for and endfor.
| Tag | Use it for |
|---|---|
if, elsif, else, unless | Conditions. unless is if not |
case, when | Comparing one value against several |
for … else | Loops, with an else that renders when the array is empty. Accepts limit:, offset: and reversed |
break, continue | Leaving a loop early, or skipping an item |
cycle | Alternating values on each iteration, such as row classes |
paginate | Splitting a collection or search results into pages |
assign, capture | Creating a variable, or capturing a block of output into one |
render | Outputting a snippet, with its own isolated variables |
section, sections | Outputting one section, or a section group such as the header |
content_for 'blocks' | Outputting a section's theme blocks in the merchant's order |
form | Product, cart, contact and customer forms with the right hidden fields |
schema, stylesheet, javascript | A section's settings, and CSS and JS scoped to it |
liquid, echo | Several tags inside one delimiter, without repeating {% %} on every line |
comment, raw, doc | Commenting out code, outputting Liquid literally, and documenting a snippet's parameters |
Inside a for loop, the forloop object tells you where you are: forloop.index counts from 1, forloop.index0 from 0, and forloop.first, forloop.last and forloop.length do what they say.
A for loop stops after 50 iterations. This is the most common reason a collection page "is missing products". Wrap it in paginate, which takes a page size from 1 to 250:
{% paginate collection.products by 24 %}
{% for product in collection.products %}
{% render 'product-card', product: product %}
{% else %}
<p>No products in this collection yet.</p>
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}
Use render, not include. include is deprecated because it leaks every variable into the snippet, which is how a harmless edit in one snippet breaks another. With render, a snippet only sees what you pass it:
{% render 'price', product: product, show_compare: true %}
Filters
A filter changes a value on its way out. Filters chain from left to right: {{ product.title | downcase | truncate: 30 }}.
| Group | Filters | Example |
|---|---|---|
| Money | money, money_with_currency, money_without_currency, money_without_trailing_zeros | {{ product.price | money }} gives $10.00, from a stored 1000 |
| Images | image_url, image_tag | {{ product | image_url: width: 800 | image_tag }} |
| Theme assets | asset_url, stylesheet_tag, script_tag | {{ 'theme.css' | asset_url | stylesheet_tag }} |
| Text | upcase, downcase, capitalize, truncate, truncatewords, strip_html, escape, handleize, append, prepend, replace, remove, split | {{ article.content | strip_html | truncatewords: 30 }} |
| Arrays | size, first, last, join, map, where, sort, sort_natural, uniq, reverse, concat, compact | {{ collection.products | where: 'available' | size }} |
| Maths | plus, minus, times, divided_by, modulo, round, ceil, floor, at_least, at_most | {{ 7 | divided_by: 2 }} gives 3, because both numbers are integers |
| Dates | date | {{ article.published_at | date: '%-d %B %Y' }} |
| Translations | t | {{ 'products.product.add_to_cart' | t }} |
| Fallbacks | default | {{ product.vendor | default: 'Our studio' }} |
| Links | within, link_to | {{ product.url | within: collection }} |
| Metafields | metafield_tag, metafield_text | {{ product.metafields.custom.care_guide | metafield_tag }} |
| Data | json | {{ product | json }}, for handing data to JavaScript |
Three of these deserve a warning.
- Money. Prices are stored in the currency's smallest unit, so 1000 is $10.00. Printing
product.pricewithoutmoneyshows the raw number. - Images.
image_urlreturns an error unless you give it awidthor aheight, up to 5760 pixels. The oldimg_urlfilter still appears in tutorials; it is deprecated. Pipeimage_urlintoimage_tagand Shopify writes thesrcsetfor you. - Division.
divided_byrounds down when both numbers are integers. Divide by2.0if you need the decimal.
Metafields and metaobjects
Metafields are custom fields on products, collections, customers and the rest: a care guide, a size chart, a list of related products. Metaobjects are your own content types, such as testimonials or store locations, with as many entries as you like.
{% # The formatted value, ready to print %}
{{ product.metafields.custom.care_guide }}
{% # The raw typed value %}
{{ product.metafields.custom.care_guide.value }}
{% # A list of product references %}
{% for related in product.metafields.custom.related_products.value %}
{% render 'product-card', product: related %}
{% endfor %}
{% # Every entry of a metaobject type %}
{% for testimonial in metaobjects.testimonials.values %}
<blockquote>{{ testimonial.quote }}</blockquote>
{% endfor %}
The .value part is what trips people up. Without it you get the metafield itself, formatted for display; with it you get the underlying data, which is what you loop over. And before writing any of this by hand, check whether the theme editor's dynamic sources already let you connect the metafield to a section setting without code.
Where every theme file lives
| Folder | Holds | Notes |
|---|---|---|
layout/ | theme.liquid, the wrapper around every page | Must output content_for_header in the head and content_for_layout in the body |
templates/ | One JSON file per page type: product.json, collection.json, index.json | JSON lists which sections appear, so merchants rearrange them in the editor. Alternates such as product.gift.json render with ?view=gift |
sections/ | Section files, plus section groups such as header-group.json and footer-group.json | A section has its own settings in its schema tag |
blocks/ | Theme blocks that any section can accept | Can nest inside each other, up to eight levels |
snippets/ | Reusable fragments | Output with render |
assets/ | CSS, JavaScript, fonts, images | Linked with asset_url |
config/ | settings_schema.json defines theme settings; settings_data.json holds their values | Editing settings_data.json by hand is overwritten the next time someone saves in the editor |
locales/ | Translation strings, such as en.default.json | Read with the t filter |
A minimal section, with a setting the merchant can change and a preset so it appears in the "Add section" list:
<div class="announcement">
{{ section.settings.text }}
</div>
{% schema %}
{
"name": "Announcement",
"settings": [
{ "type": "text", "id": "text", "label": "Text", "default": "Free delivery over ₹999" }
],
"presets": [{ "name": "Announcement" }]
}
{% endschema %}
Shopify CLI commands
The CLI is how theme work should happen: on your machine, in version control, against a copy of the theme rather than the live one. Install it with npm install -g @shopify/cli.
| Command | Does |
|---|---|
shopify theme init | Starts a new theme from Shopify's reference theme |
shopify theme pull --store your-store | Downloads a theme from the store |
shopify theme dev --store your-store | Serves a local preview at http://127.0.0.1:9292 with your real store data, reloading as you save |
shopify theme check | Lints the theme for errors, deprecated tags and performance problems |
shopify theme push --unpublished | Uploads the theme as a new, unpublished copy |
shopify theme push | Overwrites an existing theme. Choose carefully which one |
shopify theme share | Uploads an unpublished copy and gives you a preview link to send to someone |
shopify theme publish | Makes a theme live |
Run theme check before every push. It catches the deprecated include and img_url above, missing translation keys, and markup that blocks rendering.
Store URLs worth knowing
Every Shopify store answers the same set of URLs, which is useful for testing, for building links, and for debugging someone else's store.
| URL | Returns |
|---|---|
/collections/all | Every product in the store |
/products/blue-t-shirt.js | That product as JSON, for JavaScript |
/cart.js | The current cart as JSON |
/cart/add.js, /cart/change.js, /cart/clear.js | The cart endpoints themes use to add, update and empty the cart |
/cart/12345:1,67890:2 | A cart permalink: a link that fills the cart with those variant IDs and quantities |
/discount/SPRING10 | Applies a discount code to the visitor's session, then shows the home page |
/products/blue-t-shirt?view=gift | The product through the alternate template product.gift.json |
/search?q=linen | Search results |
/sitemap.xml | The sitemap Shopify generates and keeps up to date for you |
/robots.txt | Generated too, and editable through a robots.txt.liquid template if you really need to |
Store setup checklist
For owners rather than developers: the settings we check on every store before it takes its first order. The labels are the ones in the Shopify admin today.
Settings → General. Store name, contact email, address and the store currency. The currency is hard to change once orders exist.
Settings → Payments. Shopify Payments is the simplest option where it is offered. It was not available to stores based in India at the time of writing, so Indian stores connect a third-party provider such as Razorpay or PayU. Place a real test order whichever you use.
Settings → Markets. Which countries you sell to, and in which currencies and languages. Each market can have its own prices and domain.
Settings → Shipping and delivery. Shipping profiles, zones and rates. Check that every product sits in a profile that ships where you sell.
Settings → Taxes and duties. Shopify calculates the tax, but registering and filing are still your responsibility, including GST for Indian stores.
Settings → Checkout. Checkout fields, customer accounts and the checkout's look in the checkout editor. checkout.liquid has been retired: the checkout pages in 2024, and the thank-you and order status pages in August 2025. Customisation now means checkout UI extensions, usually through an app. Shopify Scripts were scheduled to stop on 30 June 2026, with Shopify Functions as the replacement, so check that any discount or shipping logic you rely on has moved.
Settings → Policies. Refund, privacy, terms and shipping policies. Payment providers expect them, and customers look for them.
Settings → Notifications. Order confirmation and shipping emails, and the address they are sent from.
Settings → Customer events. Tracking pixels live here, rather than pasted into the theme. Google Analytics connects through Shopify's Google & YouTube app.
Online Store → Preferences. The home page title and meta description, and the password page. Removing the password is the last step before launch.
Getting found on Google
Shopify does more of this for you than most platforms, so the work is mostly not undoing it.
- Write each product's own listing. Every product, collection and page has a search engine listing section at the bottom of its edit screen for the title and description Google shows. The defaults are your product title and the first lines of the description.
- Keep the redirect when you change a handle. Shopify offers to create a URL redirect when you edit a handle. Leave it ticked, or every link to the old URL breaks.
- Describe your images. Every image in the admin has a place for alt text. Screen readers use it, and so does image search.
- Leave the canonical tag alone. Themes print
{{ canonical_url }}intheme.liquid, which tells Google that/collections/shirts/products/blue-t-shirtand/products/blue-t-shirtare the same page. - Submit the sitemap once.
/sitemap.xmlgoes into Google Search Console, and Shopify keeps it current after that.
Common mistakes
- Editing the live theme. Duplicate it first, or work locally with the CLI. A typo in
theme.liquidtakes the whole storefront down. - Too many apps. Each one is usually a monthly fee and a script on every page, and some leave code in the theme after you uninstall them. Review the list every quarter.
- Printing prices without
money, and wondering why a ₹1,499 product shows 149900. - Looping a collection without
paginate, and losing everything after the 50th product. - Following old tutorials.
include,img_urlandcheckout.liquidall still appear in search results. If a snippet uses them, it predates the current platform. - Changing handles without redirects. Every changed URL without a redirect is a broken link in Google and in every email you ever sent.
Most of this is manageable in-house. Where it stops being so is product data structure, checkout changes, integrations with the other systems you run, and custom theme code you would have to carry through every theme update. That is the work our Shopify development team does. If the store is built and simply needs someone keeping apps, theme updates and speed in order, Shopify store maintenance covers that from ₹4,999 / $79 a month.
