{"title":"Theme debugging","slug":"theme-debugging","url":"https://support.storeconnect.com/articles/theme-debugging","url_markdown":"https://support.storeconnect.com/articles/theme-debugging.md","subtitle":null,"summary":"Debug Liquid templates with the debug and timer tags, which output to the real-time Console panel: platform differences from Shopify Liquid, gotchas around caching, pagination, multi-store URLs, and special variable scoping rules.","type":"Developer_Documentation","video_url":"","keywords":"theme debugging, debug tag, timer tag, console, liquid debugging, cache, pagination, multi-store, Shopify differences, whitespace control, default tag, records vs drops","last_modified":"2026-08-21T07:12:35+0000","body_markdown":"StoreConnect provides two template-level debugging tags — `{% debug %}` and `{% timer %}` — whose output appears in the **Console**, a real-time debugging panel connected to the storefront via WebSocket. The Console shows debug lines, timers, errors, warnings, and the full template rendering tree for each request.\n\n## The `{% debug %}` tag\n\nLogs key-value pairs to the Console. Use it to inspect variables, check conditions, and trace template execution.\n\n\n```liquid\n\n{% debug product_name: current_product.name, product_id: current_product.id %}\n```\n\n\nThe Console shows:\n\n```\n\nproduct_name: Widget Pro\nproduct_id: a1B2c3D4e5F6g7H8\n```\n\n`{% debug %}` does **not** output anything to the page — it is safe to leave in templates during development.\n\n### Common debugging patterns\n\n**Check if a variable exists:**\n\n\n```liquid\n\n{% debug\n  has_product: current_product != blank,\n  has_customer: current_customer != blank,\n  cart_empty: current_cart.empty?\n%}\n```\n\n\n**Inspect drop properties:**\n\n\n```liquid\n\n{% debug\n  product_name: current_product.name,\n  product_price: current_product.price,\n  product_available: current_product.available,\n  variant_count: current_product.variants.size\n%}\n```\n\n\n**Trace execution path:**\n\n\n```liquid\n\n{% debug step: \"before variant check\", variant_types: current_product.variant_types.size %}\n{% if current_product.variant_types.size \u003e 0 %}\n  {% debug step: \"has variants\", first_type: current_product.variant_types.first.name %}\n{% else %}\n  {% debug step: \"no variants\" %}\n{% endif %}\n```\n\n\n**Inspect request data:**\n\n\n```liquid\n\n{% debug\n  path: current_request.path,\n  params_q: current_request.params.q,\n  params_page: current_request.params.page,\n  method: current_request.method\n%}\n```\n\n\n**Inspect form state:**\n\n\n```liquid\n\n{% form \"add-to-cart\", product_id: current_product.id %}\n  {% debug\n    form_errors_count: form.errors.size,\n    quantity_name: form.quantity.name,\n    quantity_value: form.quantity.value\n  %}\n{% endform %}\n```\n\n\n**Inspect search or filter data:**\n\n\n```liquid\n\n{% debug\n  search_type: current_search.type,\n  search_total: current_search.total,\n  field_count: current_search.fields.size\n%}\n{% for field in current_search.fields %}\n  {% debug field_name: field.name, option_count: field.options.size %}\n{% endfor %}\n```\n\n\n**Check custom data fields:**\n\n\n```liquid\n\n{% debug\n  custom_tier: current_customer.data.loyalty_tier,\n  custom_notes: current_order.data.special_instructions\n%}\n```\n\n\n### Inspect available fields on a drop\n\nUse `record_fields` and `record_relationships` to discover what properties are available on a drop. Filters cannot be used directly inside `{% debug %}` tag parameters, so assign them first:\n\n\n```liquid\n\n{%- assign product_fields = current_product | record_fields | join: \", \" -%}\n{%- assign product_rels = current_product | record_relationships | join: \", \" -%}\n{%- assign product_type = current_product | record_name -%}\n{% debug product_fields: product_fields, product_rels: product_rels, type: product_type %}\n```\n\n\n### Dump an object as JSON\n\n\n```liquid\n\n{%- assign cart_json = current_cart | json -%}\n{%- assign fields_json = current_search.fields | json -%}\n{% debug cart_json: cart_json, search_fields: fields_json %}\n```\n\n\n### Conditional debug output to the page\n\nIf you do not have Console access, output debug info directly to the page (remove before production):\n\n\n```liquid\n\n{% if current_request.params.debug == \"true\" %}\n  \u003cpre style=\"background:#111;color:#0f0;padding:1em;font-size:12px;overflow:auto\"\u003e\n    product: {{ current_product | json }}\n    cart items: {{ current_cart.item_count }}\n    customer: {{ current_customer.name | default: \"not logged in\" }}\n  \u003c/pre\u003e\n{% endif %}\n```\n\n\nVisit the page with `?debug=true` to see the output.\n\n## The `{% timer %}` tag\n\nWraps a block of template code and measures execution time in milliseconds. Results appear in the Console's timer section.\n\n\n```liquid\n\n{% timer \"product_render\" %}\n  {% render \"products/product/images\", product: current_product %}\n  {% render \"products/product/details\", product: current_product %}\n{% endtimer %}\n```\n\n\nConsole shows:\n\n```\n\nproduct_render: 45.2ms\n```\n\n### Performance debugging patterns\n\n**Identify the slowest section of a page:**\n\n\n```liquid\n\n{% timer \"header\" %}\n  {% render \"header\" %}\n{% endtimer %}\n\n{% timer \"main_content\" %}\n  {{ body_content }}\n{% endtimer %}\n\n{% timer \"footer\" %}\n  {% render \"footer\" %}\n{% endtimer %}\n```\n\n\n**Narrow down within a slow section:**\n\n\n```liquid\n\n{% timer \"product_images\" %}\n  {% render \"products/product/images\", product: current_product %}\n{% endtimer %}\n\n{% timer \"product_variants\" %}\n  {% render \"products/product/variant_selector\", product: current_product %}\n{% endtimer %}\n\n{% timer \"related_products\" %}\n  {% render \"products/related_products\", product: current_product %}\n{% endtimer %}\n```\n\n\n**Measure cache effectiveness:**\n\n\n```liquid\n\n{% timer \"product_uncached\" %}\n  {% render \"products/product/full\", product: current_product %}\n{% endtimer %}\n\n{% timer \"product_cached\" %}\n  {% cache \"product\", items: [current_product, current_store] %}\n    {% render \"products/product/full\", product: current_product %}\n  {% endcache %}\n{% endtimer %}\n```\n\n\n:::note\nTimer handles are static strings, not dynamic expressions. Each iteration of a loop reports under the same handle — the Console shows cumulative time, which is useful for identifying whether a loop is the bottleneck.\n:::\n\n## Debugging steps\n\n1. **Reproduce the issue** — identify the exact page, URL, and conditions.\n2. **Add `{% debug %}` tags** to inspect relevant variables near the problem area.\n3. **Check the Console** for debug output, errors, and warnings.\n4. **If it is a performance issue**, wrap sections in `{% timer %}` to find the slow part.\n5. **Narrow down** — move debug or timer tags closer to the root cause.\n6. **Check the data** — use `record_fields` to verify what properties exist on a drop.\n7. **Fix the issue** and remove debug/timer tags (or leave them if useful for ongoing monitoring).\n\n## Common gotchas\n\n### Multi-store URL routing\n\nStoreConnect supports multiple stores under a single installation. When stores share a domain, every URL is prefixed with the store path:\n\n```\n\nhttps://example.com/store-a/products\nhttps://example.com/store-b/cart\n```\n\nAlways use URL helpers or the `params` filter rather than hardcoding paths. Hardcoded paths break in multi-store setups.\n\n### Template resolution order\n\nTemplates resolve in this order:\n\n1. Client theme — the store's custom theme.\n2. Base theme — the default StoreConnect theme.\n\nThis means:\n- Client themes only need files they want to override.\n- Removing a file from the client theme restores the base theme version.\n- New base theme files are automatically available unless overridden.\n\n### Pagination\n\nCollections must be wrapped in `{% paginate %}` to limit results:\n\n\n```liquid\n\n{% paginate all_products by 20 %}\n  {% for product in all_products %}\n    {{ product.name }}\n  {% endfor %}\n{% endpaginate %}\n```\n\n\n:::warning\nWithout `{% paginate %}`, collections may return all records, which can be very slow on stores with large catalogs.\n:::\n\nThe `paginate` tag accepts:\n- `by` — items per page (required)\n- `as` — custom URL parameter name (default: `page`)\n- `window` — number of page links to show (default: 5)\n\n### Cache behavior\n\nThe `{% cache %}` tag caches rendered HTML fragments:\n\n\n```liquid\n\n{% cache \"product\", items: [current_product, current_store, current_customer, current_cart] %}\n  {%- comment -%} Expensive rendering {%- endcomment -%}\n{% endcache %}\n```\n\n\nAlways include all objects that affect the output in the `items` list:\n- Include `current_customer` if the output varies by logged-in user.\n- Include `current_cart` if the output shows cart state.\n- Use `expires_in` for time-based expiration.\n\n### Records vs drops\n\nThe `{% query %}` tag returns Record objects, not Drops. They are different:\n\n| | Records | Drops |\n|---|---------|-------|\n| Custom fields | `record.custom_data.Field__c` | `drop.data.field_name` |\n| Field names | API names | Lowercase, normalized |\n| Methods | Basic field access | Rich computed properties |\n| Conversion | `record \\| cast: \"Product\"` | Already a drop |\n\nUse `cast` to convert a Record to a Drop when you need drop methods:\n\n\n```liquid\n\n{% query 'Product2' as raw_products %}\n{% for record in raw_products %}\n  {%- assign product = record | cast: \"Product\" -%}\n  {{ product.price | money }}\n{% endfor %}\n```\n\n\n### Custom data fields\n\nAll major objects support custom data fields. Access them via the `data` property on drops:\n\n\n```liquid\n\n{{ product.data.custom_field_name }}\n{{ order.data.special_instructions }}\n{{ current_customer.data.loyalty_tier }}\n```\n\n\nOn Record objects (from `{% query %}`), use `custom_data` instead, with the field's API name (including the `__c` suffix) as mapped in your Custom Data Mappings configuration:\n\n\n```liquid\n\n{% query 'Product2' as products %}\n{% for record in products %}\n  {{ record.custom_data.Custom_Field__c }}\n{% endfor %}\n```\n\n\nSee [Add custom data fields to your store](liquid-custom-data-fields) for how to configure Custom Data Mappings and the full list of supported field types.\n\n### Whitespace control\n\nLiquid tags support whitespace trimming with `-`:\n\n\n```liquid\n\n{%- assign x = \"hello\" -%}\n{%- if true -%}\n  Content\n{%- endif -%}\n```\n\n\nThe `-` trims whitespace on the side it appears. Use it liberally to prevent unwanted whitespace in HTML output.\n\n### `body_content` vs `current_page.body_content`\n\nThese are different variables:\n\n\n```liquid\n\n{%- comment -%} In layouts/theme.liquid — the fully rendered page template output {%- endcomment -%}\n{{ body_content }}\n\n{%- comment -%} In pages/home.liquid — the page's CMS content blocks {%- endcomment -%}\n{{ current_page.body_content }}\n```\n\n\n### The `{% default %}` tag vs the `| default:` filter\n\nThese are different:\n\n\n```liquid\n\n{%- comment -%} Tag: sets variable if not already defined {%- endcomment -%}\n{% default show_price: true, max_items: 10 %}\n\n{%- comment -%} Filter: provides fallback for nil or empty values {%- endcomment -%}\n{{ product.description | default: \"No description\" }}\n```\n\n\nUse the tag at the top of snippets to define default parameter values. Use the filter inline to provide fallbacks for nil or blank values.\n\n## Differences from Shopify Liquid\n\nWhile StoreConnect uses the Liquid template language, there are important differences from Shopify:\n\n| Feature | Shopify | StoreConnect |\n|---------|---------|-------------|\n| Variant system | Fixed (up to 3 options) | N-ary (unlimited variant types) |\n| Data access | Section/block settings | Drops and custom data fields |\n| Components | Sections with JS API | Reloadable components with events |\n| Controllers | None | Liquid controllers (before/after/final) |\n| Forms | `{% form %}` (limited types) | `{% form %}` (30+ types) |\n| Querying | Limited Liquid access | `{% query %}` tag for direct queries |\n| Session storage | Not available | `{% session %}` tag |\n| Structs | Not available | `{% struct %}` tag |\n| Object creation | Not available | `{% new %}` tag (UUID, List, Map, Rand) |\n| Custom fields | Metafields | `data.*` property on all drops |\n| Themes | File-based only | File-synced to database |\n| Layouts | Single layout | Multiple layouts with `{% layout %}` |\n| Content blocks | Sections/blocks | CMS content blocks with types |\n| Search | Limited | Full search with trait filters |\n\n## Markdown rendering\n\nContent fields (page body, article body, product descriptions) support Markdown rendering. The content is converted to HTML before output. Use the `sc-rich-text` CSS class on the container for proper styling:\n\n\n```liquid\n\n\u003cdiv class=\"sc-rich-text\"\u003e\n  {{ current_page.body_content }}\n\u003c/div\u003e\n```\n"}