Theme debugging
On this page
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.
The {% debug %} tag
Logs key-value pairs to the Console. Use it to inspect variables, check conditions, and trace template execution.
```liquid
{% debug product_name: current_product.name, product_id: current_product.id %} ```
The Console shows:
```
product_name: Widget Pro product_id: a1B2c3D4e5F6g7H8 ```
{% debug %} does not output anything to the page — it is safe to leave in templates during development.
Common debugging patterns
Check if a variable exists:
```liquid
{% debug has_product: current_product != blank, has_customer: current_customer != blank, cart_empty: current_cart.empty? %} ```
Inspect drop properties:
```liquid
{% debug product_name: current_product.name, product_price: current_product.price, product_available: current_product.available, variant_count: current_product.variants.size %} ```
Trace execution path:
```liquid
{% debug step: “before variant check”, variant_types: current_product.variant_types.size %} {% if current_product.variant_types.size > 0 %} {% debug step: “has variants”, first_type: current_product.variant_types.first.name %} {% else %} {% debug step: “no variants” %} {% endif %} ```
Inspect request data:
```liquid
{% debug path: current_request.path, params_q: current_request.params.q, params_page: current_request.params.page, method: current_request.method %} ```
Inspect form state:
```liquid
{% form “add-to-cart”, product_id: current_product.id %} {% debug form_errors_count: form.errors.size, quantity_name: form.quantity.name, quantity_value: form.quantity.value %} {% endform %} ```
Inspect search or filter data:
```liquid
{% debug search_type: current_search.type, search_total: current_search.total, field_count: current_search.fields.size %} {% for field in current_search.fields %} {% debug field_name: field.name, option_count: field.options.size %} {% endfor %} ```
Check custom data fields:
```liquid
{% debug custom_tier: current_customer.data.loyalty_tier, custom_notes: current_order.data.special_instructions %} ```
Inspect available fields on a drop
Use 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:
```liquid
{%- assign product_fields = current_product | record_fields | join: “, “ -%} {%- assign product_rels = current_product | record_relationships | join: “, “ -%} {%- assign product_type = current_product | record_name -%} {% debug product_fields: product_fields, product_rels: product_rels, type: product_type %} ```
Dump an object as JSON
```liquid
{%- assign cart_json = current_cart | json -%} {%- assign fields_json = current_search.fields | json -%} {% debug cart_json: cart_json, search_fields: fields_json %} ```
Conditional debug output to the page
If you do not have Console access, output debug info directly to the page (remove before production):
```liquid
{% if current_request.params.debug == “true” %}
product: {{ current_product | json }}
cart items: {{ current_cart.item_count }}
customer: {{ current_customer.name | default: "not logged in" }}
{% endif %} ```
Visit the page with ?debug=true to see the output.
The {% timer %} tag
Wraps a block of template code and measures execution time in milliseconds. Results appear in the Console’s timer section.
```liquid
{% timer “product_render” %} {% render “products/product/images”, product: current_product %} {% render “products/product/details”, product: current_product %} {% endtimer %} ```
Console shows:
```
product_render: 45.2ms ```
Performance debugging patterns
Identify the slowest section of a page:
```liquid
{% timer “header” %} {% render “header” %} {% endtimer %}
{% timer “main_content” %} {{ body_content }} {% endtimer %}
{% timer “footer” %} {% render “footer” %} {% endtimer %} ```
Narrow down within a slow section:
```liquid
{% timer “product_images” %} {% render “products/product/images”, product: current_product %} {% endtimer %}
{% timer “product_variants” %} {% render “products/product/variant_selector”, product: current_product %} {% endtimer %}
{% timer “related_products” %} {% render “products/related_products”, product: current_product %} {% endtimer %} ```
Measure cache effectiveness:
```liquid
{% timer “product_uncached” %} {% render “products/product/full”, product: current_product %} {% endtimer %}
{% timer “product_cached” %} {% cache “product”, items: [current_product, current_store] %} {% render “products/product/full”, product: current_product %} {% endcache %} {% endtimer %} ```
:::note Timer 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. :::
Debugging steps
- Reproduce the issue — identify the exact page, URL, and conditions.
- Add
{% debug %}tags to inspect relevant variables near the problem area. - Check the Console for debug output, errors, and warnings.
- If it is a performance issue, wrap sections in
{% timer %}to find the slow part. - Narrow down — move debug or timer tags closer to the root cause.
- Check the data — use
record_fieldsto verify what properties exist on a drop. - Fix the issue and remove debug/timer tags (or leave them if useful for ongoing monitoring).
Common gotchas
Multi-store URL routing
StoreConnect supports multiple stores under a single installation. When stores share a domain, every URL is prefixed with the store path:
```
https://example.com/store-a/products https://example.com/store-b/cart ```
Always use URL helpers or the params filter rather than hardcoding paths. Hardcoded paths break in multi-store setups.
Template resolution order
Templates resolve in this order:
- Client theme — the store’s custom theme.
- Base theme — the default StoreConnect theme.
This means: - Client themes only need files they want to override. - Removing a file from the client theme restores the base theme version. - New base theme files are automatically available unless overridden.
Pagination
Collections must be wrapped in {% paginate %} to limit results:
```liquid
{% paginate all_products by 20 %} {% for product in all_products %} {{ product.name }} {% endfor %} {% endpaginate %} ```
:::warning
Without {% paginate %}, collections may return all records, which can be very slow on stores with large catalogs.
:::
The paginate tag accepts:
- by — items per page (required)
- as — custom URL parameter name (default: page)
- window — number of page links to show (default: 5)
Cache behavior
The {% cache %} tag caches rendered HTML fragments:
```liquid
{% cache “product”, items: [current_product, current_store, current_customer, current_cart] %} {%- comment -%} Expensive rendering {%- endcomment -%} {% endcache %} ```
Always include all objects that affect the output in the items list:
- Include current_customer if the output varies by logged-in user.
- Include current_cart if the output shows cart state.
- Use expires_in for time-based expiration.
Records vs drops
The {% query %} tag returns Record objects, not Drops. They are different:
| Records | Drops | |
|---|---|---|
| Custom fields | record.custom_data.Field__c |
drop.data.field_name |
| Field names | API names | Lowercase, normalized |
| Methods | Basic field access | Rich computed properties |
| Conversion | record \| cast: "Product" |
Already a drop |
Use cast to convert a Record to a Drop when you need drop methods:
```liquid
{% query ‘Product2’ as raw_products %} {% for record in raw_products %} {%- assign product = record | cast: “Product” -%} {{ product.price | money }} {% endfor %} ```
Custom data fields
All major objects support custom data fields. Access them via the data property on drops:
```liquid
{{ product.data.custom_field_name }} {{ order.data.special_instructions }} {{ current_customer.data.loyalty_tier }} ```
On 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:
```liquid
{% query ‘Product2’ as products %} {% for record in products %} {{ record.custom_data.Custom_Field__c }} {% endfor %} ```
See Add custom data fields to your store for how to configure Custom Data Mappings and the full list of supported field types.
Whitespace control
Liquid tags support whitespace trimming with -:
```liquid
{%- assign x = “hello” -%} {%- if true -%} Content {%- endif -%} ```
The - trims whitespace on the side it appears. Use it liberally to prevent unwanted whitespace in HTML output.
body_content vs current_page.body_content
These are different variables:
```liquid
{%- comment -%} In layouts/theme.liquid — the fully rendered page template output {%- endcomment -%} {{ body_content }}
{%- comment -%} In pages/home.liquid — the page’s CMS content blocks {%- endcomment -%} {{ current_page.body_content }} ```
The {% default %} tag vs the | default: filter
These are different:
```liquid
{%- comment -%} Tag: sets variable if not already defined {%- endcomment -%} {% default show_price: true, max_items: 10 %}
{%- comment -%} Filter: provides fallback for nil or empty values {%- endcomment -%} {{ product.description | default: “No description” }} ```
Use the tag at the top of snippets to define default parameter values. Use the filter inline to provide fallbacks for nil or blank values.
Differences from Shopify Liquid
While StoreConnect uses the Liquid template language, there are important differences from Shopify:
| Feature | Shopify | StoreConnect |
|---|---|---|
| Variant system | Fixed (up to 3 options) | N-ary (unlimited variant types) |
| Data access | Section/block settings | Drops and custom data fields |
| Components | Sections with JS API | Reloadable components with events |
| Controllers | None | Liquid controllers (before/after/final) |
| Forms | {% form %} (limited types) |
{% form %} (30+ types) |
| Querying | Limited Liquid access | {% query %} tag for direct queries |
| Session storage | Not available | {% session %} tag |
| Structs | Not available | {% struct %} tag |
| Object creation | Not available | {% new %} tag (UUID, List, Map, Rand) |
| Custom fields | Metafields | data.* property on all drops |
| Themes | File-based only | File-synced to database |
| Layouts | Single layout | Multiple layouts with {% layout %} |
| Content blocks | Sections/blocks | CMS content blocks with types |
| Search | Limited | Full search with trait filters |
Markdown rendering
Content 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:
```liquid
```
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.