Skip to content
Store slow, dated, or hard to edit? Request a Store Diagnostic
Running B2B on spreadsheets or email? Plan Your B2B Project
App & Plus Dev

Shopify Liquid Tips Every Developer Should Know

Liquid looks simple for the first afternoon you work with it, then quietly stops being simple the first time you’re debugging why a collection page is timing out, why a metafield renders as an empty object instead of the string you expected, or why a theme editor setting isn’t showing up where it should. None of that is Liquid being badly designed, it’s a constrained templating language with real performance and rendering rules most tutorials skip over.

This post is written for developers who already know the basics of {% if %}, {% for %}, and {{ output }} and want the next layer: filters that don’t get enough attention, patterns that actually affect page performance, debugging techniques that work in a real Online Store 2.0 theme, and the specific mechanics of sections, JSON templates, and app blocks that trip people up when they move from legacy .liquid templates to the current architecture.

Everything below reflects how Liquid and Shopify’s theme architecture genuinely behave, where something depends on your specific theme setup or Shopify’s current documentation for an exact detail, that’s flagged rather than guessed at.

Useful (and Underused) Liquid Filters

Most developers lean on {% if %} and basic string filters and never touch the filters that actually make templates cleaner and safer.

| json

Converts any Liquid object into a JSON string. It’s the single most useful debugging filter in Liquid, more on that below, and it’s also genuinely useful in production for passing Liquid data into a <script> tag for JavaScript to consume, since it escapes the output safely.

<script>
 window.productData = {{ product | json }};
</script>

| where

Filters an array down to items where a given property matches a value. Extremely useful for filtering a collection’s products or a metaobject list without writing a manual {% for %} loop with an {% if %} inside it.

{% assign sale_products = collection.products | where: "compare_at_price", null %}

Note the direction: where keeps items that match, so filtering out sale items versus keeping only sale items depends on which value you’re comparing against.

| map

Pulls a single property out of every item in an array, returning a new array of just those values. Handy for building a list of tags, variant titles, or IDs without a manual loop.

{% assign all_vendors = collection.products | map: "vendor" | uniq %}

| sort_by

Sorts an array by a property, similar to where and map in that it saves you writing manual loop-based sorting logic. Combine with | reverse for descending order.

{% assign newest_first = collection.products | sort_by: "created_at" | reverse %}

| default

Returns a fallback value if the input is nil, false, or an empty string, genuinely useful for metafields and settings that might not be filled in, and cleaner than a manual {% if %} check for a simple fallback.

{{ product.metafields.custom.short_description | default: product.description | strip_html | truncate: 150 }}

| money, | money_with_currency, and related formatting filters

Never hand-format a price string yourself. Shopify’s money filters respect the store’s currency formatting settings, and | money_with_currency is the one to reach for anywhere the currency isn’t otherwise obvious, multi-currency Markets setups, email templates, or anything outside the main storefront context.

image_url (and the legacy img_url filter)

image_url is the current, recommended filter for generating responsive image URLs with width/height parameters, use it in new theme code. You’ll still see img_url in older themes and third-party snippets; it still works in most contexts but is the legacy filter, and Shopify’s tooling points developers toward image_url, particularly when paired with srcset.

<img
 src="{{ product.featured_image | image_url: width: 800 }}"
 srcset="{{ product.featured_image | image_url: width: 400 }} 400w,
 {{ product.featured_image | image_url: width: 800 }} 800w"
 alt="{{ product.featured_image.alt | escape }}"
 loading="lazy"
 width="800"
 height="{{ 800 | divided_by: product.featured_image.aspect_ratio }}"
>

| escape, | strip_html, | truncate, and | handleize

Small and unglamorous, but responsible for preventing a large share of real theme bugs, unescaped HTML breaking a layout, an unstripped metafield dumping raw tags into a meta description, or a manually built handle not matching Shopify’s actual format. Use them by default on any dynamic text output that isn’t already known-safe.

Performance Patterns That Actually Matter

Liquid runs server-side on every request, so inefficient template code has a direct, measurable cost on page load, not just a theoretical one.

Avoid heavy loops over all products or collections

Looping through collections.all.products, or nesting a full product loop inside a collection loop, is the classic Liquid performance mistake. Liquid has object access limits and render time limits per request, and a nested “for every collection, loop every product” pattern is the fastest way to hit them on a store with any real catalogue size. Paginate, use limit, and avoid re-fetching the same object collection more than once per template.

Use the {% liquid %} tag for cleaner multi-line logic

The {% liquid %} tag lets you write a block of tag-only logic (assign, if, for, case) without repeating {% and %} on every line. It doesn’t change performance on its own, but it reduces whitespace-control mistakes and makes multi-step logic far easier to review in a pull request.

{% liquid
 assign featured = collection.products | where: "tags", "featured"
 assign has_featured = featured.size > 0
 if has_featured
 assign display_products = featured
 else
 assign display_products = collection.products
 endif
%}

Understand section and block rendering costs

Online Store 2.0’s “sections everywhere” model means a typical page is built from many independently rendered sections and blocks rather than one monolithic template. Each section is its own Liquid render, and each app block adds another. That’s good for merchant flexibility and generally fine for performance, but a page stacked with many sections, especially ones each running their own product or collection loops, adds up. Be deliberate about how many sections a page actually needs before blaming “the theme” generically for slow load times.

Avoid unnecessary render calls inside loops

{% render %} creates an isolated variable scope, which is exactly what you want for reusable snippets, but calling render once per iteration inside a large loop (say, once per product in a 50-item grid) has a real, cumulative cost compared to a single loop with shared logic where isolation isn’t genuinely needed. Reserve render in loops for cases where isolation actually matters, not as a default habit for every repeated element.

Don’t repeat expensive filters unnecessarily

Filters like sort_by or where re-evaluate the full array they’re given every time they’re called. Assigning the result once at the top of a template or section, rather than calling the same filter chain again lower down “just to be safe,” avoids doing the same work twice for no reason.

Debugging Techniques for Liquid and Themes

Dump objects with | json

The fastest way to understand what data is actually available on an object, a product, a line item, a metafield, a section’s block settings, is to dump it directly during development.

<pre>{{ product | json }}</pre>

Wrap it in a check so it never ships to production ({% if request.design_mode %} inside the theme editor, or simply removing it before commit), but treat this as the default first move whenever a value isn’t rendering the way you expect, rather than guessing at property names from memory.

Use Shopify CLI’s theme check

Shopify CLI includes Theme Check, a static analysis tool built specifically for Liquid and theme JSON. Run via shopify theme check from the theme’s root directory, it catches issues before they reach a live store: undefined objects, deprecated filters and tags (including legacy img_url usage), missing translation keys, and JSON schema errors in section files. Wiring it into a pre-commit hook or CI step catches a category of bug that’s otherwise only found by clicking through the storefront manually.

Use shopify theme dev for local development

The Shopify CLI’s local development server proxies your theme against a real store’s data while serving your local file changes with hot reload, which is a materially faster debugging loop than uploading through the theme editor every time you change a line of Liquid.

Use browser dev tools for Online Store 2.0 rendering

Because sections and app blocks each render independently, the Network tab is genuinely useful for Online Store 2.0 debugging in a way it wasn’t for older themes, you can see individual section-rendering requests when the theme editor triggers a re-render, which helps isolate whether a bug is in a section’s Liquid, an app block, or the surrounding layout. Inspecting the rendered DOM against the schema settings is usually the fastest way to confirm whether a setting genuinely isn’t being read, or is being read correctly with a display-logic problem elsewhere.

Online Store 2.0 Specifics Every Liquid Developer Should Understand

Sections are no longer homepage-only

Under Online Store 2.0, sections can be added to any template, not just the homepage, product, collection, and even cart pages can be built from stacked, reorderable sections rather than a single fixed .liquid file. For anyone maintaining an older theme, this is usually the biggest structural difference to plan around.

JSON templates define structure, not just content

Templates now live as .json files (for example, templates/product.json) defining which sections appear, in what order, with what settings and blocks, rather than a .liquid file containing the entire page’s markup. The rendering logic still lives in each section’s .liquid file under sections/; the JSON template is a configuration manifest referencing those sections.

App blocks need to be genuinely supported, not just tolerated

For a section to accept app blocks (a merchant adding an app’s functionality, a review widget, an upsell block, inside a section via the editor), its schema needs a {% schema %} definition that explicitly allows "type": "@app" blocks. Custom sections built without this won’t offer that option, which is a common cause of “why can’t I add this app here” support questions.

Schema settings and presets drive merchant-facing flexibility

Every section’s {% schema %} block defines the settings, blocks, and presets a merchant sees in the theme editor. Getting this right, sensible defaults, clear labels, sane presets, is as much a part of good Liquid development as the render logic itself, since it determines whether a merchant can use what you built without needing a developer every time.

A Quick Liquid Code Review Checklist

Before shipping theme code, run through this:

  1. Every loop over products or collections uses limit or pagination where the catalogue could realistically be large.
  2. No render calls inside large loops unless variable isolation is genuinely required.
  3. Dynamic output that isn’t already known-safe is passed through escape, strip_html, or an equivalent filter.
  4. New image output uses image_url with explicit width/height and srcset, not the legacy img_url filter.
  5. {% schema %} blocks include sensible presets and, where relevant, "type": "@app" block support.
  6. shopify theme check runs clean, or any remaining warnings are deliberate and understood.
  7. Debug output (| json dumps, console logs) is removed or gated before merging.
  8. Multi-step logic uses {% liquid %} rather than a long chain of individually tagged lines, for readability in review.

When to Bring in a Specialist

A lot of Liquid work is genuinely manageable for a competent front-end developer working from Shopify’s documentation. Where it stops being a reasonable DIY project is custom section architecture for a complex catalogue (nested product options, multi-vendor marketplaces, heavy metafield-driven content), performance issues that persist after the obvious loop and render fixes, or migrating a legacy .liquid theme to a genuine Online Store 2.0 structure without breaking existing functionality. That’s the kind of build and rebuild work covered under Shopify web design and development, theme architecture handled by developers who work in Liquid and Online Store 2.0 daily.

Frequently Asked Questions

What’s the actual difference between img_url and image_url?
img_url is the legacy filter for generating image URLs with size parameters; image_url is the current, recommended filter, generally paired with srcset for responsive images. Both still work in most themes, but Shopify’s own tooling, including Theme Check, flags img_url usage as outdated, so new code should use image_url.

Can I just use JavaScript instead of Liquid for dynamic content?
For genuinely client-side interactivity (cart updates, filtering without a page reload, animations), yes, JavaScript is the right tool. But anything that needs to be correct on first render, pricing, availability, SEO-relevant content, should come from Liquid server-side rendering rather than being injected afterward by JavaScript, both for performance and so search engines and non-JS contexts see accurate content immediately.

How do I debug a Liquid error that doesn’t show a clear message?
Start by isolating the section or snippet, comment out blocks of logic until the error disappears, then reintroduce them one at a time. Dumping the relevant object with {{ object | json }} early in that process usually reveals whether the issue is a missing property, an unexpected data type, or genuinely a logic mistake in the template.

Is Online Store 2.0 mandatory for all Shopify themes now?
Shopify continues to support older theme structures, but new theme development, most theme store submissions, and the full range of app block and section flexibility are built around Online Store 2.0. If you’re doing any meaningful new development, building or upgrading to an Online Store 2.0-compatible structure is the practical default rather than an optional extra.

What’s the best way to actually learn Liquid properly as a new Shopify developer?
Working directly in a real theme with shopify theme dev running locally, and deliberately dumping objects with | json to see the real data shape rather than guessing from documentation examples, gets you fluent faster than reading reference docs in isolation. Running Theme Check from day one also teaches good habits early rather than needing to unlearn them later.

Want a second set of eyes on your theme?

If you’re inheriting a Liquid codebase that’s hard to maintain, or planning a genuine Online Store 2.0 rebuild rather than another patch, a Shopify audit is a solid starting point to see exactly where the theme’s architecture is helping you and where it’s working against you. If you already know it’s time to rebuild properly, book a call and we’ll talk through the scope.

Niraj Raut
Written by Niraj Raut SEO Manager

Niraj Raut is the SEO Manager and co-founder at Nexly. He helps Australian Shopify and Shopify Plus brands earn durable organic growth through technical SEO, search-led store architecture and content that ranks. He writes about what actually moves rankings for ecommerce.

Connect on LinkedIn
Have a Shopify project? Chat with us, takes 30 seconds.