by InlinexDev

Shopify Theme App Extensions with Liquid: A Developer's Guide

How to build Shopify Theme App Extensions using Liquid blocks, enabling merchants to add app functionality through the theme customizer.

ShopifyLiquidtheme extensionsShopify appfrontend

What Are Theme App Extensions?

Theme App Extensions let Shopify app developers inject UI into merchant themes without editing theme code directly. Merchants add your extension blocks through the theme customizer — no code changes, no compatibility issues.

This is how LogoBadge adds a Google Reviews badge to any Shopify theme.

Why Theme Extensions Over ScriptTag?

The old approach — injecting JavaScript via ScriptTag API — had problems:

  • Performance — external scripts slow down page load
  • Conflicts — scripts can conflict with theme JavaScript
  • Removal — uninstalling the app doesn't always remove the script
  • Control — merchants can't position or configure the element

Theme Extensions solve all of these. They render server-side with Liquid, load instantly, and merchants control placement.

Extension Structure

extensions/
  logo-badge/
    blocks/
      badge.liquid
    assets/
      badge-styles.css
      google-icon.svg
    locales/
      en.default.json
    shopify.extension.toml

Configuration File

# shopify.extension.toml
type = "theme"
name = "Logo Badge"

[[extensions.blocks]]
template = "blocks/badge.liquid"
name = "Google Reviews Badge"
target = "section"

Building the Liquid Block

A block is a Liquid template with a schema that defines settings:

{% comment %} blocks/badge.liquid {% endcomment %}

{% if block.settings.show_badge %}
<div class="logobadge" 
     style="text-align: {{ block.settings.alignment }}; 
            margin: {{ block.settings.margin_top }}px 0 {{ block.settings.margin_bottom }}px;">
  <a href="{{ block.settings.maps_url }}" 
     target="_blank" 
     rel="noopener noreferrer"
     class="logobadge__link">
    
    <span class="logobadge__icon">
      {{ 'google-icon.svg' | asset_url | img_tag: 'Google', 'logobadge__google-icon' }}
    </span>
    
    <span class="logobadge__rating">
      {{ block.settings.rating }}
    </span>
    
    <span class="logobadge__stars">
      {% assign full_stars = block.settings.rating | floor %}
      {% assign decimal = block.settings.rating | minus: full_stars %}
      
      {% for i in (1..full_stars) %}
        <span class="logobadge__star logobadge__star--full">&#9733;</span>
      {% endfor %}
      
      {% if decimal >= 0.5 %}
        <span class="logobadge__star logobadge__star--half">&#9733;</span>
      {% endif %}
    </span>
    
    <span class="logobadge__count">
      ({{ block.settings.review_count }})
    </span>
  </a>
</div>
{% endif %}

{{ 'badge-styles.css' | asset_url | stylesheet_tag }}

{% schema %}
{
  "name": "Google Reviews Badge",
  "target": "section",
  "settings": [
    {
      "type": "checkbox",
      "id": "show_badge",
      "label": "Show badge",
      "default": true
    },
    {
      "type": "text",
      "id": "rating",
      "label": "Google rating",
      "default": "4.8"
    },
    {
      "type": "text",
      "id": "review_count",
      "label": "Number of reviews",
      "default": "100"
    },
    {
      "type": "url",
      "id": "maps_url",
      "label": "Google Maps URL"
    },
    {
      "type": "select",
      "id": "alignment",
      "label": "Alignment",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" },
        { "value": "right", "label": "Right" }
      ],
      "default": "center"
    },
    {
      "type": "range",
      "id": "margin_top",
      "label": "Top margin",
      "min": 0,
      "max": 40,
      "step": 4,
      "unit": "px",
      "default": 8
    },
    {
      "type": "range",
      "id": "margin_bottom",
      "label": "Bottom margin",
      "min": 0,
      "max": 40,
      "step": 4,
      "unit": "px",
      "default": 8
    }
  ]
}
{% endschema %}

Updating Settings from Your App

The challenge: Google Reviews data changes over time. How does the badge stay current? The app backend periodically fetches fresh data and updates the block settings via Shopify's Admin API:

async function updateBadgeSettings(shop, accessToken, reviewData) {
  const response = await fetch(
    `https://${shop}/admin/api/2024-01/themes/${themeId}/assets.json`,
    {
      method: 'PUT',
      headers: {
        'X-Shopify-Access-Token': accessToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        asset: {
          key: 'config/settings_data.json',
          // Update the specific block settings
        }
      })
    }
  );
}

Alternatively, use metafields that the Liquid block reads dynamically:

{% assign rating = shop.metafields.logobadge.rating %}
{% assign review_count = shop.metafields.logobadge.review_count %}

Metafields are updated via the Admin API without touching theme settings:

await fetch(`https://${shop}/admin/api/2024-01/metafields.json`, {
  method: 'POST',
  headers: {
    'X-Shopify-Access-Token': accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    metafield: {
      namespace: 'logobadge',
      key: 'rating',
      value: '4.9',
      type: 'single_line_text_field'
    }
  })
});

Styling Best Practices

Namespace Everything

.logobadge { display: inline-flex; align-items: center; gap: 4px; }
.logobadge__link { text-decoration: none; color: inherit; display: inline-flex; align-items: center; gap: 4px; }
.logobadge__rating { font-weight: 600; font-size: 14px; }
.logobadge__stars { color: #fbbc04; }
.logobadge__count { color: #666; font-size: 13px; }
.logobadge__google-icon { width: 16px; height: 16px; }

Respect Theme Styles

  • Use inherit for fonts and colors where possible
  • Don't set global styles that could leak into the theme
  • Use CSS custom properties for theming:
.logobadge {
  font-family: var(--font-body-family, inherit);
  color: var(--color-foreground, #333);
}

Testing Your Extension

# Start development server
shopify app dev

# This opens a development store with your extension loaded
# Navigate to Theme Customizer to add your block

Deployment

Theme extensions are deployed alongside your Shopify app:

shopify app deploy

This pushes the extension to Shopify's CDN. Merchants who have installed your app can then add the block through their theme customizer.

Limitations

  • No JavaScript execution in the block itself (use separate ScriptTag if needed)
  • Limited to theme sections — you can't inject into arbitrary positions
  • Settings are static — for dynamic data, use metafields or App Proxy
  • One extension per app — bundle all blocks in a single extension

Conclusion

Theme App Extensions are the modern way to extend Shopify storefronts. They're performant (server-rendered), merchant-friendly (theme customizer integration), and maintainable (no theme code editing). For any Shopify app that needs a storefront presence, Theme Extensions should be your first choice.

Related Project

LogoBadge - Google Reviews

Shopify app that automatically displays a Google Reviews badge directly under the store logo, showing live star ratings and review counts from Google Business Profile.