Liquid Code Examples
Below is a consolidated reference implementation of the Rich Landing Bundle theme app extension, combining the key Liquid, CSS, and JavaScript pieces described in your documentation.
bundle-embed.liquid
liquid
1{% comment %}2 bundle-embed.liquid3 App embed block loaded via body target4{% endcomment %}5 6<style>7 rich-landing-bundle {8 display: block !important;9 position: relative;10 }11 .rlb-embed-inactive-notice {12 display: none !important;13 }14</style>15 16<script>17(function() {18 window.__rlbEmbedEnabled = true;19 var cfg = window.StandaloneBundleConfig = window.StandaloneBundleConfig || {};20 if (!cfg.appProxyPath) cfg.appProxyPath = '{{ shop.url }}/apps/rlb-bundle';21 if (!cfg.shopDomain) cfg.shopDomain = '{{ shop.permanent_domain }}';22 if (!cfg.currencySymbol) cfg.currencySymbol = '{{ cart.currency.symbol }}';23 if (!cfg.currencyCode) cfg.currencyCode = '{{ cart.currency.iso_code }}';24 if (!cfg.moneyFormat) cfg.moneyFormat = {{ shop.money_format | json }};25 if (!cfg.moneyFormatWithCurrency) cfg.moneyFormatWithCurrency = {{ shop.money_with_currency_format | json }};26 if (!cfg.locale) cfg.locale = '{{ request.locale.iso_code }}';27 if (!cfg.countryCode) cfg.countryCode = '{{ localization.country.iso_code }}';28 if (!cfg.availableCountries) {29 cfg.availableCountries = [30 {% for country in localization.available_countries %}31 '{{ country.iso_code }}'{% unless forloop.last %},{% endunless %}32 {% endfor %}33 ];34 }35 if (!cfg.countryCurrencyMap) {36 cfg.countryCurrencyMap = {37 {% for country in localization.available_countries %}38 "{{ country.iso_code }}": "{{ country.currency.iso_code }}"{% unless forloop.last %},{% endunless %}39 {% endfor %}40 };41 }42})();43</script>44 45<script>46(function() {47 var s = window.Shopify;48 if (!s) return;49 var cfg = window.StandaloneBundleConfig || {};50 var map = cfg.countryCurrencyMap || {};51 var activeCurrency = (s.currency && s.currency.active)52 ? s.currency.active53 : cfg.currencyCode;54 var candidateCountry = s.country || cfg.countryCode;55 if (activeCurrency && map[candidateCountry]56 && map[candidateCountry] !== activeCurrency) {57 for (var cc in map) {58 if (map[cc] === activeCurrency) {59 candidateCountry = cc;60 break;61 }62 }63 }64 cfg.countryCode = candidateCountry;65 if (activeCurrency) cfg.currencyCode = activeCurrency;66 if (s.locale) cfg.locale = s.locale;67})();68</script>69 70<script>71(function() {72 if (window.__bundleEmbedLoaded) return;73 window.__bundleEmbedLoaded = true;74 75 var cssUrl = '{{ 'rlb-bundle.css' | asset_url }}';76 var scripts = [77 '{{ 'bundle-globals.js' | asset_url }}',78 '{{ 'rlb-bundle-style.js' | asset_url }}',79 '{{ 'rlb-bundle.js' | asset_url }}'80 ];81 82 function loadAssets() {83 if (!document.querySelector('link[href="' + cssUrl + '"]')) {84 var link = document.createElement('link');85 link.rel = 'stylesheet';86 link.href = cssUrl;87 link.crossOrigin = 'anonymous';88 document.head.appendChild(link);89 }90 91 function loadScript(idx) {92 if (idx >= scripts.length) return;93 var src = scripts[idx];94 if (document.querySelector('script[src="' + src + '"]')) {95 loadScript(idx + 1);96 return;97 }98 var el = document.createElement('script');99 el.src = src;100 el.crossOrigin = 'anonymous';101 el.onload = function() { loadScript(idx + 1); };102 document.head.appendChild(el);103 }104 105 loadScript(0);106 }107 108 loadAssets();109})();110</script>rich-landing-bundle-block.liquid
liquid
1{% comment %}2 rich-landing-bundle-block.liquid3 Section block with optimized metafield path4{% endcomment %}5 6{% assign bundles_config = shop.metafields.bundle_app.bundles_config.value %}7{% assign bundle_data = bundles_config.bundles[block.settings.bundle_id] %}8 9{% if request.design_mode %}10 {{ 'bundle-globals.js' | asset_url | preload_tag: as: 'script', fetchpriority: 'high', crossorigin: 'anonymous' }}11 {{ 'rlb-bundle-style.js' | asset_url | preload_tag: as: 'script', fetchpriority: 'high', crossorigin: 'anonymous' }}12 {{ 'rlb-bundle.js' | asset_url | preload_tag: as: 'script', fetchpriority: 'high', crossorigin: 'anonymous' }}13 <script fetchpriority="high" defer src="{{ 'bundle-globals.js' | asset_url }}" crossorigin="anonymous"></script>14 <script fetchpriority="high" defer src="{{ 'rlb-bundle-style.js' | asset_url }}" crossorigin="anonymous"></script>15 <script fetchpriority="high" defer src="{{ 'rlb-bundle.js' | asset_url }}" crossorigin="anonymous"></script>16{% endif %}17 18{% if bundle_data and bundle_data.isActive %}19 {%- comment -%} Optimized path: render with template hint {%- endcomment -%}20 <rich-landing-bundle21 data-bundle-id="{{ block.settings.bundle_id }}"22 data-template="{{ bundle_data.template }}"23 data-init-fn="initRlbBundle"24 {% if request.design_mode %}data-design-mode{% endif %}25 {{ block.shopify_attributes }}26 >27 <!-- Skeleton HTML for loading state -->28 <div class="rlb-skeleton">29 <div class="rlb-skeleton-header"></div>30 <div class="rlb-skeleton-body"></div>31 </div>32 </rich-landing-bundle>33{% else %}34 {%- comment -%} Fallback path: render without template hint {%- endcomment -%}35 <rich-landing-bundle36 data-bundle-id="{{ block.settings.bundle_id }}"37 data-init-fn="initRlbBundle"38 {% if request.design_mode %}data-design-mode{% endif %}39 {{ block.shopify_attributes }}40 >41 <div class="rlb-skeleton">42 <div class="rlb-skeleton-header"></div>43 <div class="rlb-skeleton-body"></div>44 </div>45 </rich-landing-bundle>46{% endif %}47 48<script>49(function(){50 var c = window.StandaloneBundleConfig;51 var id = {{ block.settings.bundle_id | json }};52 if (!c || !id) return;53 var u = c.appProxyPath + '/config/' + id;54 var params = [];55 if (c.countryCode) params.push('country=' + c.countryCode);56 if (c.locale) params.push('locale=' + c.locale);57 if (params.length) u += '?' + params.join('&');58 window.__bundlePrefetch = window.__bundlePrefetch || {};59 window.__bundlePrefetch[id] = fetch(u, {60 method: 'GET',61 headers: {62 'Accept': 'application/json',63 'Content-Type': 'application/json'64 }65 }).then(function(r) { return r.json(); });66})();67</script>68 69{% schema %}70{71 "name": "Rich Landing Bundle",72 "target": "section",73 "settings": [74 {75 "type": "text",76 "id": "bundle_id",77 "label": "Bundle ID",78 "info": "Enter the bundle ID from your app dashboard. Find this in the app under Bundles > Edit Bundle."79 }80 ]81}82{% endschema %}bundle-globals.js
javascript
1// bundle-globals.js2// Shared utilities, Preact runtime, and global helpers3 4window.RLB = window.RLB || {};5 6// Example: simple event bus used by bundle widgets7(function(ns) {8 var listeners = {};9 10 ns.on = function(event, cb) {11 (listeners[event] = listeners[event] || []).push(cb);12 };13 14 ns.off = function(event, cb) {15 if (!listeners[event]) return;16 listeners[event] = listeners[event].filter(function(fn) { return fn !== cb; });17 };18 19 ns.emit = function(event, payload) {20 (listeners[event] || []).forEach(function(fn) { fn(payload); });21 };22})(window.RLB);23 24// Money formatting helper using StandaloneBundleConfig25window.RLB.formatMoney = function(cents, opts) {26 var cfg = window.StandaloneBundleConfig || {};27 var format = (opts && opts.withCurrency)28 ? cfg.moneyFormatWithCurrency29 : cfg.moneyFormat;30 if (!format) return (cents / 100).toFixed(2);31 var value = (cents / 100).toFixed(2);32 return format.replace("{{amount}}", value).replace("{{ amount }}", value);33};rlb-bundle-style.js
javascript
1// rlb-bundle-style.js2// Dynamic style injection based on widget configuration3 4(function() {5 var cfg = window.StandaloneBundleConfig || {};6 var theme = cfg.bundleTheme || {};7 8 var css = "" +9 ":root {" +10 (theme.primaryColor ? "--rlb-primary: " + theme.primaryColor + ";" : "") +11 (theme.secondaryColor ? "--rlb-secondary: " + theme.secondaryColor + ";" : "") +12 (theme.fontFamily ? "--rlb-font-family: " + theme.fontFamily + ";" : "") +13 "}" +14 "rich-landing-bundle { font-family: var(--rlb-font-family, inherit); }";15 16 var styleEl = document.createElement('style');17 styleEl.type = 'text/css';18 styleEl.appendChild(document.createTextNode(css));19 document.head.appendChild(styleEl);20})();rlb-bundle.js
javascript
1// rlb-bundle.js2// Main widget logic and custom element definition3 4(function() {5 if (window.customElements && customElements.get('rich-landing-bundle')) return;6 7 function fetchConfig(bundleId) {8 var c = window.StandaloneBundleConfig || {};9 if (window.__bundlePrefetch && window.__bundlePrefetch[bundleId]) {10 return window.__bundlePrefetch[bundleId];11 }12 var u = c.appProxyPath + '/config/' + bundleId;13 var params = [];14 if (c.countryCode) params.push('country=' + c.countryCode);15 if (c.locale) params.push('locale=' + c.locale);16 if (params.length) u += '?' + params.join('&');17 return fetch(u, {18 method: 'GET',19 headers: {20 'Accept': 'application/json',21 'Content-Type': 'application/json'22 }23 }).then(function(r) { return r.json(); });24 }25 26 class RichLandingBundle extends HTMLElement {27 constructor() {28 super();29 this.bundleId = this.getAttribute('data-bundle-id');30 this.template = this.getAttribute('data-template');31 this.attachShadow({ mode: 'open' });32 }33 34 connectedCallback() {35 if (!this.bundleId) return;36 this.renderSkeleton();37 var self = this;38 fetchConfig(this.bundleId).then(function(cfg) {39 self.config = cfg;40 self.render();41 }).catch(function() {42 self.renderError();43 });44 }45 46 renderSkeleton() {47 this.shadowRoot.innerHTML = "<style>.rlb-skeleton{opacity:.4}</style><div class='rlb-skeleton'>Loading bundle...</div>";48 }49 50 renderError() {51 this.shadowRoot.innerHTML = "<div class='rlb-error'>Unable to load bundle.</div>";52 }53 54 render() {55 var cfg = this.config || {};56 var items = cfg.items || [];57 var html = "<div class='rlb-bundle'>" +58 "<h3>" + (cfg.title || "Bundle") + "</h3>" +59 "<ul>" +60 items.map(function(it) {61 return "<li>" + it.title + " - " + window.RLB.formatMoney(it.price) + "</li>";62 }).join("") +63 "</ul>" +64 "</div>";65 this.shadowRoot.innerHTML = html;66 }67 }68 69 if (window.customElements) {70 customElements.define('rich-landing-bundle', RichLandingBundle);71 }72})();