Skip to main content

GemPages

Design custom product and landing pages to optimize conversions

Written by Andrey Gadashevich

GemPages is a Shopify page builder that lets you design custom product and landing pages to optimize conversions.

Because GemPages product pages are custom-built, the RecurrinGO subscription widget is added with a small code snippet placed in a custom code block, rather than through the usual selector setup. You paste the snippet in the GemPages builder and set a few values in its config section.

⚠️ Note: Depending on how your page is built, this integration may need some manual adjustment. If the widget doesn't appear or behaves unexpectedly, our support team can help — reach out via live chat or at [email protected]

How it works

The snippet:

  1. renders RecurrinGO's native subscription widget on the page — the exact same widget (frequencies, discounts, save badges, prices, translations) that RecurrinGO shows on regular theme product pages. Nothing is hand-built, so it always matches your RecurrinGO rule;

  2. reads the product, variant and price automatically from the theme;

  3. reuses the page's existing "Add to cart" button — when a subscription frequency is selected, clicking it adds the item as a subscription; a one-time selection uses the page's normal add-to-cart.

Because the widget comes from the app, there is no manual frequency / selling plan configuration — it's driven entirely by your RecurrinGO rule (store, collection or product scoped).

Prerequisites: Before you start

The widget appears only when all of these are true:

  1. RecurrinGO is installed on your store.

  2. The RecurrinGO app embed is enabled in your live theme (Shopify admin → Online Store → Themes → Customize → App embeds → enable RecurrinGO). This is what loads RecurrinGO on GemPages pages.

  3. There is an active RecurrinGO rule that covers the product on the page.

If any of these is missing, the page stays a normal product page (fail-soft) and a diagnostic message is logged to the browser console.

The full code to paste is in Step 2 below.

Installation

Step 1 — Add a Custom HTML element

In the GemPages editor, drag a Custom HTML / Liquid (Code) element onto the page where the subscription options should appear — typically next to the variant selector or just above the Add to cart button.

Step 2 — Paste the snippet

Copy the entire block below and paste it into the Custom HTML element.

<div id="ros-gp-widget" hidden>
<div id="ros-gp-widget-slot"></div>
<!-- Fallback button — only shown if no existing ATC button is reused. -->
<button type="button" id="ros-gp-atc" style="display:none;margin-top:10px">Add to cart</button>
<p id="ros-gp-status" role="status" style="min-height:1em;margin:6px 0"></p>
</div>

<script>
(function () {
'use strict';

// ============================ CONFIG ============================
var CONFIG = {
// Leave null to auto-detect from window.ShopifyAnalytics.meta. Set explicitly
// only to override (e.g. a fixed variant on a landing page).
variantId: null,
productId: null,

// CSS selector of the page's existing "Add to cart" button to reuse.
// Leave null to default to the app-configured selector (api.getAtcSelector()).
atcSelector: null,

quantity: 1,

// What to do after a successful subscribe add so the theme cart reflects it.
// The public API never reloads the page — the integrator decides.
// 'reload' | 'cart' (go to /cart) | 'none' (leave it to your own cart refresh)
afterAdd: 'cart',

// How long to wait for the RecurrinGO bundle before giving up (ms).
readyTimeoutMs: 8000
};
// ================================================================

// ---- Resolve product data from the theme meta ---------------------------
// Returns {productId, variantId, price} — price in CENTS (Shopify meta units),
// or null if the current product/variant can't be determined.
function resolveProduct() {
var meta = (window.ShopifyAnalytics && window.ShopifyAnalytics.meta) || {};
var product = meta.product || {};
var variants = product.variants || [];

var productId = CONFIG.productId || product.id || null;
var variantId = CONFIG.variantId
|| meta.selectedVariantId
|| (variants[0] && variants[0].id)
|| null;

if (!variantId) { return null; }

var variant = variants.find(function (v) { return v.id === +variantId; }) || variants[0] || {};
var price = typeof variant.price === 'number' ? variant.price : null;

return { productId: productId, variantId: +variantId, price: price };
}

// ---- API readiness (event first, then poll, then time out to null) -------
// Returns Promise<api|null>. null == RecurrinGO not installed / embed off /
// bundle not loaded on this page — always feature-detect, never assume.
function whenRosApiReady(timeoutMs) {
function pick() {
var s = window.Spurit || {};
var key = Object.keys(s).find(function (k) { return s[k] && s[k].publicMethods; });
return key ? s[key].publicMethods : null;
}

return new Promise(function (resolve) {
var existing = pick();
if (existing) { resolve(existing); return; }

var done = false;
function finish(api) { if (!done) { done = true; resolve(api); } }

document.addEventListener('spurit:ros:public-methods-ready', function (e) {
var name = e && e.detail && e.detail.appName;
var api = name && window.Spurit[name] && window.Spurit[name].publicMethods;
finish(api || pick());
}, { once: true });

// Fallback poll in case the event fired before we subscribed.
var waited = 0, step = 200;
var iv = setInterval(function () {
var api = pick();
if (api) { clearInterval(iv); finish(api); return; }
waited += step;
if (waited >= timeoutMs) { clearInterval(iv); finish(null); }
}, step);
});
}

// ---- Cart helpers --------------------------------------------------------
function afterAdd() {
if (CONFIG.afterAdd === 'reload') { window.location.reload(); }
else if (CONFIG.afterAdd === 'cart') { window.location.href = '/cart'; }
// 'none' → integrator refreshes their own cart drawer here.
}

// Add the variant as a subscription (single /cart/add.js via the public API).
function subscribe(api, product, sellingPlanId) {
return api.addToCart({
variantId: product.variantId,
productId: product.productId,
sellingPlanId: sellingPlanId,
quantity: CONFIG.quantity
});
}

// ---- ATC wiring ----------------------------------------------------------
// Reuse the page's existing ATC button: intercept its click in the capture
// phase and, ONLY when a subscription frequency is selected, add as a
// subscription instead of the page's native add. One-time selection is left to
// the page's own button. Returns true if an existing button was wired.
function wireExistingAtc(api, product, handle, atcSelector) {
if (!atcSelector || !document.querySelector(atcSelector)) { return false; }

document.addEventListener('click', function (e) {
var button = e.target.closest && e.target.closest(atcSelector);
if (!button) { return; }

var sellingPlanId = handle.getSellingPlanId(); // null => one-time selected
if (!sellingPlanId) { return; } // let the page add the one-time item natively

// Stop the page's own add-to-cart, run ours instead.
e.preventDefault();
e.stopImmediatePropagation();

subscribe(api, product, sellingPlanId).then(afterAdd).catch(function (err) {
console.warn('[ros-gp] subscribe failed:', err && err.message ? err.message : err);
});
}, true); // capture: runs before the button's own handlers

return true;
}

// Fallback: render our own button when no existing ATC button is reused.
function wireOwnAtc(api, product, handle) {
var atcBtn = document.getElementById('ros-gp-atc');
var status = document.getElementById('ros-gp-status');
atcBtn.style.display = 'block';

atcBtn.addEventListener('click', async function () {
atcBtn.disabled = true;
status.textContent = 'Adding…';
try {
var sellingPlanId = handle.getSellingPlanId();
if (sellingPlanId) {
await subscribe(api, product, sellingPlanId);
} else {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ id: product.variantId, quantity: CONFIG.quantity })
}).then(function (r) { if (!r.ok) { throw new Error('add.js failed: ' + r.status); } });
}
status.textContent = 'Added to cart.';
afterAdd();
} catch (err) {
status.textContent = 'Error: ' + (err && err.message ? err.message : err);
atcBtn.disabled = false;
}
});
}

// ---- Widget wiring -------------------------------------------------------
function initWidget(api, product) {
var root = document.getElementById('ros-gp-widget');
var slot = document.getElementById('ros-gp-widget-slot');
if (!root || !slot) { return Promise.resolve(false); }

return api.renderWidget({
element: slot,
variantId: product.variantId,
productId: product.productId,
price: product.price
}).then(function (handle) {
if (!handle) {
// No active rule covers this product — leave the page as a plain product.
return false;
}

var atcSelector = CONFIG.atcSelector
|| (typeof api.getAtcSelector === 'function' ? api.getAtcSelector() : null);

if (!wireExistingAtc(api, product, handle, atcSelector)) {
wireOwnAtc(api, product, handle);
}

root.hidden = false;
return true;
});
}

// ---- Boot ----------------------------------------------------------------
whenRosApiReady(CONFIG.readyTimeoutMs).then(function (api) {
if (!api) {
// RecurrinGO not available on this page — fail soft, keep the page usable.
console.warn('[ros-gp] RecurrinGO public API not available (app not installed, embed off, or unsupported page).');
return;
}
if (typeof api.renderWidget !== 'function') {
// Older app build without the native-widget renderer — fail soft.
console.warn('[ros-gp] RecurrinGO renderWidget is unavailable — update the app to a build that exposes it.');
return;
}

var product = resolveProduct();
if (!product) {
console.warn('[ros-gp] Could not resolve product/variant from the theme meta — set CONFIG.variantId/productId manually.');
return;
}

initWidget(api, product).catch(function (err) {
console.warn('[ros-gp] Failed to render RecurrinGO widget:', err);
});
});
})();
</script>

Step 3 — (Optional) adjust the configuration

The snippet works with zero configuration on a standard product page. Edit the CONFIG block at the top of the snippet only if you need to override a default:

Field

Default

When to change it

variantId

auto (theme meta)

Pin a specific variant, e.g. a single-product landing page.

productId

auto (theme meta)

Needed only if auto-detection fails; required to match collection/product-scoped rules.

atcSelector

app-configured selector

The page's Add to cart button differs from the one set in RecurrinGO settings, or you want a specific button.

quantity

1

Add more than one unit per click.

afterAdd

'cart'

'reload' to refresh in place, 'none' if your theme has an AJAX cart drawer you refresh yourself.

readyTimeoutMs

8000

Slow stores where RecurrinGO loads late.

Step 4 — Publish and test

Save and publish the GemPages page, open it on the storefront, and:

  • confirm the subscription widget appears;

  • switch between one-time and a subscription frequency and check the prices;

  • add to cart with a subscription selected and confirm the cart line shows the subscription.

Finding product / variant IDs (only if you pin them)

Auto-detection covers the normal case; set the IDs manually only for landing pages or when detection fails.

  • Product ID — open the product in Shopify admin; it's the number at the end of the URL: admin.shopify.com/store/<store>/products/1234567890.

  • Variant ID — on the storefront product page, pick the variant and read ?variant=9876543210 from the URL, or use the product JSON at /products/<handle>.js.

Troubleshooting

Open the browser console (F12) on the storefront page and look for [ros-gp] messages:

Console message

Cause

Fix

RecurrinGO public API not available

App not installed, embed off, or bundle didn't load on this page

Enable the RecurrinGO app embed (Prerequisite 2).

renderWidget is unavailable — update the app…

The store is on an older app build

Update RecurrinGO to a build that exposes renderWidget.

Could not resolve product/variant from the theme meta

Not a product-context page (e.g. a generic landing page)

Set CONFIG.variantId (and productId) manually.

Widget renders but nothing happens on Add to cart

The page's button isn't matched by atcSelector, or the builder handles the click in a way the snippet can't intercept

Set CONFIG.atcSelector to the page's button; if it still doesn't work, the snippet's own fallback button is used.

No widget, no error

No active rule covers this product

Create/enable a RecurrinGO rule for the product, collection, or store.

Notes & limitations

  • Prices come from the theme meta (ShopifyAnalytics.meta). If a page doesn't expose them, the widget still renders with its frequencies and badges; only the price figures are blank.

  • Custom (customer-chosen) frequency rules are not supported by this embedded widget — use a rule with fixed frequencies.

  • Reusing the page's Add to cart button relies on intercepting its click. This works for standard button/form add-to-cart. If a particular GemPages layout triggers add-to-cart another way, set CONFIG.atcSelector or rely on the fallback button the snippet renders.

  • This integration may need some manual adjustment depending on the page structure. If you get stuck, contact RecurrinGO support via live chat or email.

Need help?

If you have any questions or need help with the setup, please contact our support team.

📩 Contact Support: Reach out to us via live chat or send an email to [email protected]

Did this answer your question?