Storefront Widgets
PromoSync populates rich metafield data that you can display on your storefront. This guide shows how to implement common widgets using Liquid templates.
Looking for the no-code approach? PromoSync offers drag-and-drop app extension blocks that require no theme code changes. See Theme App Extensions.
Overview
PromoSync stores product data in Shopify metafields under the psrestful namespace. These metafields can be accessed in your theme’s Liquid templates to create dynamic displays.
Tier Pricing Block
Display quantity-based pricing tiers on product pages. Tier pricing is stored at the variant level in part_price_array.
The Data Structure
// variant.metafields.psrestful.part_price_array
[
{"quantityMin": 24, "price": 1250},
{"quantityMin": 48, "price": 1100},
{"quantityMin": 144, "price": 950},
{"quantityMin": 288, "price": 825}
]Note: Prices are stored as integers in cents for fast and exact calculations (e.g., 1250 = $12.50).
Basic Implementation
Add to your product template (sections/main-product.liquid or similar):
{% raw %}{% assign tiers = product.selected_or_first_available_variant.metafields.psrestful.part_price_array.value %}
{% if tiers and tiers.size > 0 %}
<div class="tier-pricing-widget">
<h3>Volume Pricing</h3>
<table class="tier-pricing-table">
<thead>
<tr>
<th>Quantity</th>
<th>Price Each</th>
<th>You Save</th>
</tr>
</thead>
<tbody>
{% assign base_price = tiers.first.price %}
{% for tier in tiers %}
{% assign savings = base_price | minus: tier.price | times: 100 | divided_by: base_price %}
<tr>
<td>{{ tier.quantityMin }}+</td>
<td>{{ tier.price | divided_by: 100.0 | money }}</td>
<td>
{% if savings > 0 %}{{ savings }}%{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}{% endraw %}Styling the Tier Pricing Table
.tier-pricing-widget {
margin: 20px 0;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
}
.tier-pricing-widget h3 {
margin: 0 0 15px 0;
font-size: 16px;
font-weight: 600;
}
.tier-pricing-table {
width: 100%;
border-collapse: collapse;
}
.tier-pricing-table th,
.tier-pricing-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #eee;
}
.tier-pricing-table th {
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
color: #666;
}
.tier-pricing-table td:last-child {
color: #2a9d3a;
font-weight: 500;
}Compact Tier Display
For a more compact display:
{% raw %}{% assign tiers = product.selected_or_first_available_variant.metafields.psrestful.part_price_array.value %}
{% if tiers and tiers.size > 0 %}
<div class="tier-badges">
{% for tier in tiers %}
<span class="tier-badge">
<strong>{{ tier.quantityMin }}+</strong>
<span>{{ tier.price | divided_by: 100.0 | money }}</span>
</span>
{% endfor %}
</div>
{% endif %}{% endraw %}Location Decorations Block
Display available decoration methods and locations.
The Data Structure
// product.metafields.psrestful.location_decorations
[
{
"locationId": 70,
"decorations": [
{
"default": false,
"decorationId": 950,
"priceIncludes": false,
"decorationName": "Embroidery",
"maxImprintColors": 99
},
{
"default": true,
"decorationId": 951,
"priceIncludes": false,
"decorationName": "Screen Print",
"maxImprintColors": 6
}
],
"locationName": "FRONT",
"locationRank": 2,
"maxDecoration": 0,
"minDecoration": 0,
"defaultLocation": false,
"decorationsIncluded": 0
},
{
"locationId": 71,
"decorations": [
{
"default": false,
"decorationId": 950,
"priceIncludes": false,
"decorationName": "Embroidery",
"maxImprintColors": 99
}
],
"locationName": "LEFT CHEST",
"locationRank": 1,
"maxDecoration": 0,
"minDecoration": 0,
"defaultLocation": true,
"decorationsIncluded": 0
}
]Implementation
{% raw %}{% assign locations = product.metafields.psrestful.location_decorations.value %}
{% if locations and locations.size > 0 %}
<div class="decoration-options">
<h3>Decoration Options</h3>
<div class="decoration-locations">
{% for location in locations %}
<div class="decoration-location">
<h4>{{ location.locationName }}{% if location.defaultLocation %} (Default){% endif %}</h4>
<ul class="decoration-methods">
{% for dec in location.decorations %}
<li>
<span class="method-name">{{ dec.decorationName }}</span>
{% if dec.maxImprintColors %}
<span class="colors">Up to {{ dec.maxImprintColors }} colors</span>
{% endif %}
{% if dec.default %}
<span class="default-badge">Default</span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
{% endif %}{% endraw %}Styling Decorations
.decoration-options {
margin: 20px 0;
}
.decoration-locations {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.decoration-location {
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.decoration-location h4 {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: 600;
}
.location-size {
font-size: 12px;
color: #666;
margin-bottom: 10px;
}
.decoration-methods {
list-style: none;
padding: 0;
margin: 0;
}
.decoration-methods li {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #e0e0e0;
}
.decoration-methods li:last-child {
border-bottom: none;
}
.setup-charge.free {
color: #2a9d3a;
}Product Specifications Block
Display detailed product specifications.
The Data Structure
// product.metafields.psrestful.specifications
{
"value": {
"material": "100% Airlume combed and ring-spun cotton",
"weight": "4.2 oz",
"fit": "Unisex retail fit",
"features": [
"Side-seamed",
"Tear-away label",
"Shoulder-to-shoulder taping"
],
"compliance": ["CPSIA Certified", "WRAP Certified"]
}
}Implementation
{% raw %}{% assign specs = product.metafields.psrestful.specifications.value %}
{% if specs %}
<div class="product-specifications">
<h3>Specifications</h3>
<dl class="specs-list">
{% if specs.material %}
<dt>Material</dt>
<dd>{{ specs.material }}</dd>
{% endif %}
{% if specs.weight %}
<dt>Weight</dt>
<dd>{{ specs.weight }}</dd>
{% endif %}
{% if specs.fit %}
<dt>Fit</dt>
<dd>{{ specs.fit }}</dd>
{% endif %}
</dl>
{% if specs.features.size > 0 %}
<h4>Features</h4>
<ul class="features-list">
{% for feature in specs.features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
{% endif %}
{% if specs.compliance.size > 0 %}
<div class="compliance-badges">
{% for cert in specs.compliance %}
<span class="compliance-badge">{{ cert }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}{% endraw %}Supplier Information Block
Display supplier and brand information.
{% raw %}{% assign supplier = product.metafields.psrestful.supplier.value %}
{% assign brand = product.metafields.psrestful.brand.value %}
<div class="product-source">
{% if brand %}
<span class="product-brand">{{ brand }}</span>
{% endif %}
{% if supplier %}
<span class="product-supplier">from {{ supplier }}</span>
{% endif %}
</div>{% endraw %}Inventory Status Widget
Show inventory availability with more detail.
The Data Structure
// product.metafields.psrestful.inventory_data
{
"value": {
"totalAvailable": 1250,
"lastSync": "2024-01-20T14:30:00Z",
"warehouses": [
{"location": "Dallas", "qty": 500},
{"location": "Phoenix", "qty": 450},
{"location": "Atlanta", "qty": 300}
]
}
}Implementation
{% raw %}{% assign inv = product.metafields.psrestful.inventory_data.value %}
{% if inv %}
<div class="inventory-status">
{% if inv.totalAvailable > 100 %}
<span class="status in-stock">In Stock</span>
<span class="qty">{{ inv.totalAvailable }} available</span>
{% elsif inv.totalAvailable > 0 %}
<span class="status low-stock">Low Stock</span>
<span class="qty">Only {{ inv.totalAvailable }} left</span>
{% else %}
<span class="status out-of-stock">Out of Stock</span>
{% endif %}
{% if inv.warehouses.size > 1 %}
<details class="warehouse-details">
<summary>View warehouse availability</summary>
<ul>
{% for wh in inv.warehouses %}
<li>{{ wh.location }}: {{ wh.qty }} units</li>
{% endfor %}
</ul>
</details>
{% endif %}
</div>
{% endif %}{% endraw %}Theme App Extension (Online Store 2.0)
For Online Store 2.0 themes, create a theme app extension block.
Block Schema
Create blocks/tier-pricing.liquid:
{% raw %}{% assign tiers = product.selected_or_first_available_variant.metafields.psrestful.part_price_array.value %}
{% if tiers and tiers.size > 0 %}
<div class="tier-pricing-block" {{ block.shopify_attributes }}>
{% if block.settings.show_title %}
<h3 class="tier-pricing-title">{{ block.settings.title }}</h3>
{% endif %}
<table class="tier-pricing-table">
<thead>
<tr>
<th>{{ block.settings.quantity_label }}</th>
<th>{{ block.settings.price_label }}</th>
</tr>
</thead>
<tbody>
{% for tier in tiers %}
<tr>
<td>{{ tier.quantityMin }}+</td>
<td>{{ tier.price | divided_by: 100.0 | money }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% schema %}
{
"name": "Tier Pricing",
"target": "section",
"settings": [
{
"type": "checkbox",
"id": "show_title",
"label": "Show title",
"default": true
},
{
"type": "text",
"id": "title",
"label": "Title",
"default": "Volume Pricing"
},
{
"type": "text",
"id": "quantity_label",
"label": "Quantity column label",
"default": "Quantity"
},
{
"type": "text",
"id": "price_label",
"label": "Price column label",
"default": "Price Each"
}
]
}
{% endschema %}{% endraw %}JavaScript Enhancements
Dynamic Price Calculator
Add a quantity-based price calculator:
// tier-pricing-calculator.js
class TierPricingCalculator {
constructor(element) {
this.element = element;
this.tiers = JSON.parse(element.dataset.tiers);
this.quantityInput = element.querySelector('[data-quantity-input]');
this.priceDisplay = element.querySelector('[data-price-display]');
this.totalDisplay = element.querySelector('[data-total-display]');
this.bindEvents();
}
bindEvents() {
this.quantityInput.addEventListener('input', () => this.calculate());
}
calculate() {
const quantity = parseInt(this.quantityInput.value) || 0;
const tier = this.findTier(quantity);
if (tier) {
// Prices are in cents, convert to dollars
const priceInDollars = tier.price / 100;
const total = priceInDollars * quantity;
this.priceDisplay.textContent = this.formatMoney(priceInDollars);
this.totalDisplay.textContent = this.formatMoney(total);
}
}
findTier(quantity) {
for (let i = this.tiers.length - 1; i >= 0; i--) {
if (quantity >= this.tiers[i].quantityMin) {
return this.tiers[i];
}
}
return this.tiers[0];
}
formatMoney(dollars) {
return '$' + dollars.toFixed(2);
}
}
// Initialize
document.querySelectorAll('[data-tier-calculator]').forEach(el => {
new TierPricingCalculator(el);
});Usage in Template
{% raw %}{% assign tiers = product.selected_or_first_available_variant.metafields.psrestful.part_price_array.value %}
{% if tiers %}
<div data-tier-calculator data-tiers='{{ tiers | json }}'>
<input type="number" data-quantity-input value="24" min="1">
<span>Price: <span data-price-display></span></span>
<span>Total: <span data-total-display></span></span>
</div>
{% endif %}{% endraw %}Note: The JavaScript calculator receives prices in cents and should convert to dollars for display.
Best Practices
1. Check for Data Existence
Always check if metafield data exists before rendering:
{% raw %}{% assign tiers = variant.metafields.psrestful.part_price_array.value %}
{% if tiers and tiers.size > 0 %}
<!-- Render widget -->
{% endif %}{% endraw %}2. Provide Fallbacks
Show graceful fallbacks when data is missing.
3. Keep It Fast
Avoid complex calculations in Liquid - use JavaScript if needed.
4. Mobile-First Design
Ensure widgets work well on mobile devices.
5. Test with Multiple Products
Test widgets with products that have varying amounts of data.
Troubleshooting
Widget Not Showing
- Verify the product has the metafield data
- Check the metafield namespace (
psrestful) - Ensure the product was synced recently
- Check for Liquid syntax errors
Data Not Updating
- Trigger an inventory sync
- Check last sync timestamp
- Verify supplier connection
Styling Issues
- Check for CSS conflicts with your theme
- Use specific selectors to avoid conflicts
- Test in theme preview mode