Most Shopify theme bugs aren't dramatic — they're small, repeated habits that pass a quick visual check but cause broken layouts, slow pages, or settings that silently do nothing. The good news: nearly all of them are caught by Theme Check, Shopify's official linter, and are easy to fix once you know the pattern.
Here are 15 of the most common ones, each with the wrong code, the right code, and why it matters. To verify the correct syntax as you go, keep the Liquid Cheatsheet open.
1. Using {% include %} instead of {% render %}
include is deprecated. It shares the parent scope, which leads to subtle variable-leak bugs. render runs in an isolated scope — safer and faster.
{# ✗ #}
{% include 'product-card' %}
{# ✓ #}
{% render 'product-card', product: product %}
With render you pass exactly what the snippet needs. Caught by: ConvertIncludeToRender.
2. Deprecated image filter img_url
img_url is deprecated in favor of image_url, which requires an explicit width (and lets you build responsive images).
{# ✗ #}
<img src="{{ product.featured_image | img_url: '500x' }}">
{# ✓ #}
{{ product.featured_image | image_url: width: 500 | image_tag }}
Caught by: DeprecatedFilter (auto-correctable with --auto-correct).
3. Deprecated color filter hex_to_rgba
{# ✗ #}
color: {{ settings.color_name | hex_to_rgba: 0.5 }};
{# ✓ #}
color: {{ settings.color_name | color_modify: 'alpha', 0.5 }};
Use color_modify and color_to_rgb for color manipulation. Caught by: DeprecatedFilter.
4. Hardcoding URLs instead of the routes object
Hardcoded paths break on stores with locale or currency subpaths (Shopify Markets).
{# ✗ #}
<a href="/cart">Cart</a>
<a href="/collections/all">Shop</a>
{# ✓ #}
<a href="{{ routes.cart_url }}">Cart</a>
<a href="{{ routes.all_products_collection_url }}">Shop</a>
Caught by: HardcodedRoutes.
5. Missing width and height on images
Images without dimensions cause layout shift (CLS), which hurts Core Web Vitals and SEO.
{# ✗ #}
<img src="{{ image | image_url: width: 800 }}">
{# ✓ — image_tag adds width/height automatically #}
{{ image | image_url: width: 800 | image_tag }}
Caught by: ImgWidthAndHeight.
6. Not using responsive srcset
Serving one large image to every device wastes bandwidth and slows LCP. image_tag builds a responsive srcset for you:
{{ image | image_url: width: 1500 | image_tag:
widths: '400, 800, 1200, 1500',
sizes: '(min-width: 750px) 50vw, 100vw' }}
7. Outputting metafields without .value
A metafield is an object. Output it directly and you won't get the data.
{# ✗ #}
{{ product.metafields.custom.subtitle }}
{# ✓ #}
{{ product.metafields.custom.subtitle.value }}
See the full breakdown in Metafields in Liquid.
8. Invalid or duplicated {% schema %}
A section may contain exactly one {% schema %}, it must be valid JSON, and it can't contain comments, trailing commas, or Liquid. A single trailing comma stops the section from saving.
{
"name": "Hero",
"settings": [
{ "type": "text", "id": "title", "label": "Title" }
]
}
Caught by: ValidSchema / LiquidHTMLSyntaxError. Avoid it entirely by building schema with the Schema Builder.
9. Statically rendering sections that should be in a JSON template
Statically rendered sections ({% section 'name' %}) can't be reordered or removed by merchants, and a single instance is shared everywhere it appears. For anything a merchant should control, reference the section from a JSON template instead.
{# ✗ in a .liquid template for editable content #}
{% section 'featured-product' %}
Use static rendering only for genuinely fixed, non-editable placements.
10. Forgetting nil-safety on resource objects
Resource settings and objects can be empty — a merchant may not have selected anything, or the resource was deleted.
{# ✗ — errors / empty markup when nothing is selected #}
<h2>{{ section.settings.featured_product.title }}</h2>
{# ✓ #}
{% assign p = section.settings.featured_product %}
{% if p != blank %}
<h2>{{ p.title }}</h2>
{% endif %}
11. Heavy work inside loops
Filtering or sorting a large array on every iteration is slow. Do the work once, before the loop.
{# ✗ #}
{% for product in collection.products %}
{% assign sale = collection.products | where: 'available' %}
{% endfor %}
{# ✓ #}
{% assign available = collection.products | where: 'available' %}
{% for product in available %}
...
{% endfor %}
12. Cluttering the editor instead of using visible_if
Showing every advanced setting at once overwhelms merchants. Reveal options only when relevant:
{
"type": "range", "id": "slides", "label": "Slides", "min": 1, "max": 5, "step": 1, "default": 2,
"visible_if": "{{ section.settings.layout == 'slider' }}"
}
13. Not running Theme Check
Every issue above is caught automatically. Run it locally:
shopify theme check
shopify theme check --auto-correct
It's the single highest-leverage habit for theme quality — wire it into your editor and CI.
14. Hardcoded strings instead of locale files
Hardcoded text can't be translated and breaks multi-language stores.
{# ✗ #}
<button>Add to cart</button>
{# ✓ #}
<button>{{ 'products.product.add_to_cart' | t }}</button>
Store the strings in locales/en.default.json and translate per locale.
15. Using the removed @global block type
Older themes used @global to accept app blocks. It was replaced by @app.
{# ✗ #}
"blocks": [{ "type": "@global" }]
{# ✓ #}
"blocks": [{ "type": "@app" }]
See Theme Blocks in Shopify for the current block model.
The pattern behind all of these
Almost every mistake here comes down to one of three things: using a deprecated API, assuming data is always present, or hardcoding what Shopify gives you a dynamic source for. Internalize those three instincts — prefer current filters/tags, guard with != blank, and use routes/t/settings instead of literals — and your themes will be more robust, faster, and translation-ready.
Next steps
Run Theme Check on your theme today and fix what it flags. When you're building new sections, start them clean with the Schema Builder, generate correct markup from HTML with the converter, and double-check syntax against the Liquid Cheatsheet.



