Skip to content
Log in

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

  1. Reproduce the issue — identify the exact page, URL, and conditions.
  2. Add {% debug %} tags to inspect relevant variables near the problem area.
  3. Check the Console for debug output, errors, and warnings.
  4. If it is a performance issue, wrap sections in {% timer %} to find the slow part.
  5. Narrow down — move debug or timer tags closer to the root cause.
  6. Check the data — use record_fields to verify what properties exist on a drop.
  7. 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:

  1. Client theme — the store’s custom theme.
  2. 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

{{ current_page.body_content }}

```

Was this article helpful?

Was this article helpful?