Skip to main content

Custom store detail pages or deep links with RP Promap

Show a custom detail layout for a single store right on the Store Locator page

Store Locator (ProMap) · Developer guide

Build a custom store detail page (deep-link), reusing the Store Locator map

Show a custom detail layout for a single store right on the Store Locator page, opened via a link like ?store=<id>. It reuses the data & map the app already renders — no separate API key, no server.

In this guide

Overview & limitations to know

This block reads store data from the global JS object SCASLSetting that the Store Locator app injects, picks the store matching ?store=<id> in the URL, and renders a custom detail layout — while focusing the app's map on that store.

Limitations — read first

  • No SEO. Content is JS-rendered behind a query param, so Google canonicals back to the base page and will not index one page per store. If you need real per-store SEO URLs, that's a different architecture (Shopify Metaobjects).

  • Query param, not a path. Shopify Pages don't allow sub-paths, so /pages/store-locator/store-name returns 404. Use ?store=123 only.

  • Only one opening-hours set exists in the app data. A second block (e.g. "Trade Centre") can't come from the data — hardcode it in the theme (see Step 4).

  • SCASLSetting is the app's internal structure, not a guaranteed public API — it can change between releases. This block is owned & maintained by the merchant.

How it works (keep the app's map)

The detail page is a two-column layout: left = store info, right = the actual Store Locator map (full markers + info window). Mechanism:

  • Focus the map on the store: each location-list item has a data-markerid attribute. The block finds the item matching the store and calls trigger("click") → the app pans the map and opens that marker's info window.

  • Reflow the layout: hide the app's search form, location list and title, keep the map container intact, move it into the right column, and render the detail panel on the left.

  • The map uses the app's existing Google Maps key → no separate key needed.

Verified liveThis was verified working on ProMap's own demo store (rpstorelocator.app): the map renders fully and the info window auto-opens on the selected store. See the screenshot under Expected result.

1 Get a store id to test

Open the Store Locator page in a browser, open the Console (F12), and run this to list each store's id and name:

JSON.parse(SCASLSetting.locationsRaw).map(s => ({ id: s.id, name: s.name }))

Note one id for testing (e.g. 18683934).

2 Add the block to your theme

Pick one of the two methods below. Keep the existing Store Locator app block on the page — the custom block reuses its data & map.

Method A — Custom Liquid section (quick, no theme file edits)

  1. Admin → Online Store → Themes → Customize → open the store-locator page template.

  2. Add section → Custom Liquid, placed right below the Store Locator block.

  3. Paste the code from Step 3 into the Custom Liquid box → Save.

Shopify theme customizer with the Add section picker open and Custom Liquid highlighted

Theme Customizer → Add sectionCustom Liquid, on the Store Locator page template.

Method B — Dedicated snippet (cleaner, easier to maintain — recommended)

  1. Admin → Online Store → Themes → Edit code.

  2. Snippets → Add a new snippet, name it rp-store-detail.liquid, and paste the Step 3 code.

  3. In the page's section/template, add this line after the app block renders:

{% render 'rp-store-detail' %}
Theme code editor with the snippets folder expanded, creating a new snippet

Edit code → Snippets → Add a new snippet for the detail block.

3 The code block (verified working)

Adjust the CSS/HTML inside left.innerHTML to match your mockup. The map-focus and reflow logic is verified — leave it as is.

snippets/rp-store-detail.liquid (or a Custom Liquid section)

<div id="rp-store-detail-host"></div> <script> (function () {   var id = new URLSearchParams(location.search).get("store");   if (!id) return; // không có param → để Store Locator chạy bình thường    function getStores(cb) {     if (typeof SCASLSetting !== "undefined" && SCASLSetting.locationsRaw) {       try { return cb(JSON.parse(SCASLSetting.locationsRaw)); } catch (e) {}     }     if (typeof SCASLSetting !== "undefined" && SCASLSetting.locationsUrl) { // dataset lớn → CDN       return fetch(SCASLSetting.locationsUrl).then(function (r) { return r.json(); }).then(cb).catch(function () {});     }     return setTimeout(function () { getStores(cb); }, 200);   }    function whenReady(cb, n) {     n = n || 0;     var mapOk = !!document.querySelector(".gm-style");     var listOk = !!document.querySelector(".bh-sl-loc-list li[data-markerid]");     if (mapOk && listOk) return cb();     if (n > 60) return cb();     setTimeout(function () { whenReady(cb, n + 1); }, 200);   }    getStores(function (stores) {     var idx = (stores || []).findIndex(function (x) { return String(x.id) === id; });     if (idx < 0) return;     var s = stores[idx];      var pm = document.getElementById("scasl-app-container");     if (pm) pm.scrollIntoView({ block: "center" }); // kích hoạt lazy-init map      whenReady(function () {       // 1) Focus map: click item list khớp store (pan map + mở info window)       var items = [].slice.call(document.querySelectorAll(".bh-sl-loc-list li[data-markerid]"));       var li = items.filter(function (el) {         return (el.innerText || "").toLowerCase().indexOf((s.name || "").toLowerCase()) >= 0;       })[0] || document.querySelector('.bh-sl-loc-list li[data-markerid="' + idx + '"]');       if (li) { try { (window.jQuerySCASL ? jQuerySCASL(li).trigger("click") : li.click()); } catch (e) {} }        // 2) Ẩn UI mặc định, GIỮ map       [".bh-sl-form-container", ".bh-sl-loc-list", "#scasl-tabs", ".bh-sl-title"].forEach(function (q) {         var el = document.querySelector("#scasl-app-container " + q);         if (el) el.style.display = "none";       });        // 3) Layout 2 cột: trái = chi tiết, phải = container app (map còn nguyên bên trong)       var wrap = document.createElement("div");       wrap.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:32px;max-width:1120px;margin:24px auto;align-items:start";       pm.parentNode.insertBefore(wrap, pm);       var left = document.createElement("div");       left.style.cssText = "min-width:0;line-height:1.6";       wrap.appendChild(left);       wrap.appendChild(pm); // đưa cả container (chứa map) sang cột phải       pm.style.cssText = "min-width:0;width:100%;margin:0";       var mapNode = document.querySelector("#bh-sl-map-container, .bh-sl-map-container");       if (mapNode) mapNode.style.cssText = "height:520px;width:100%";        // services badges       var badges = (s.tags || []).map(function (t) {         return '<span style="display:inline-block;padding:4px 12px;margin:3px 4px 0 0;border:1px solid #c00;border-radius:16px;font-size:13px;color:#c00">' + t.tag + "</span>";       }).join("");        // giờ Retail Showroom (operating_hours là chuỗi JSON, 1 bộ)       var hrs = "";       try {         var oh = JSON.parse(s.operating_hours || "{}");         hrs = Object.keys(oh).map(function (k) {           var d = oh[k];           var slot = (d.slot && d.slot[0]) ? (d.slot[0].from + "–" + d.slot[0].to) : "Closed";           return "<div><b>" + d.name + ":</b> " + (d.status ? slot : "Closed") + "</div>";         }).join("");       } catch (e) {}        // giờ Trade Centre — KHÔNG có trong data app → hardcode theo id (xem Bước 4)       var tradeHours = {         // "18683934": "Mon–Fri: 06:00–17:00<br>Sat: 06:00–14:00<br>Sun: CLOSED",       };       var trade = tradeHours[id] || "";        left.innerHTML =         '<a href="/pages/store-locator" style="color:#c00;text-decoration:none">&larr; Back to Stores</a>' +         "<h1>" + (s.name || "") + "</h1>" +         "<p>" + (s.description || "") + "</p>" +         '<div style="margin:14px 0">' +           '<a href="#" style="background:#111;color:#fff;padding:12px 18px;text-decoration:none;margin-right:10px;display:inline-block">SELECT AT MY STORE</a>' +           '<a href="mailto:' + (s.email || "") + '" style="border:1px solid #111;color:#111;padding:12px 18px;text-decoration:none;display:inline-block">SEND AN ENQUIRY</a>' +         "</div>" +         "<p>" + [s.address, s.city, s.state, s.postal].filter(Boolean).join(", ") + "</p>" +         "<p>" + (s.shareable_link ? '<a href="' + s.shareable_link + '" target="_blank" style="color:#c00">View on Map</a>' : "") +           (s.phone ? ' &nbsp;|&nbsp; Phone <a href="tel:' + s.phone + '">' + s.phone + "</a>" : "") + "</p>" +         '<div style="display:flex;gap:40px;margin:14px 0">' +           "<div><b>Retail Showroom</b>" + hrs + "</div>" +           (trade ? "<div><b>Trade Centre</b><div>" + trade + "</div></div>" : "") +         "</div>" +         (badges ? "<p><b>Services available:</b></p><div>" + badges + "</div>" : "") +         (s.store_image ? '<img src="' + s.store_image + '" style="max-width:100%;margin-top:16px">' : "");        // map vừa đổi layout → nhắc app tính lại kích thước       if (window.google && google.maps && google.maps.event) google.maps.event.trigger(window, "resize");     });   }); })(); </script>

Why wait for .gm-style & scroll into view?

The app lazy-initialises the map when the widget scrolls into the viewport (IntersectionObserver). The block must scrollIntoView to trigger it, then wait for .gm-style (map rendered) before reflowing — otherwise the map stays blank.

4 Second hours block ("Trade Centre")

The app returns only one operating_hours set per store (= Retail Showroom). The "Trade Centre" column in the mockup can't come from the data, so you have two options:

  • (a) Drop the Trade Centre column and show a single hours block.

  • (b) Hardcode Trade Centre hours via an id lookup (the tradeHours variable is already in the code) — just fill it in:

var tradeHours = {   "18683934": "Mon–Fri: 06:00–17:00<br>Sat: 06:00–14:00<br>Sun: CLOSED",   // "<store id>": "<Trade Centre hours for that store>" };

Confirm with the merchant Decide between (a) and (b) before finalising the layout — it changes the left column's structure.

Expected result

Two-column store detail page: store info on the left, the Store Locator map with an open info window on the right

Two-column layout running live on the demo store: left = store detail (name, description, two CTAs, address, View on Map, Opening Hours, service badges, image); right = the Store Locator map with the info window auto-opened on the selected store.

Important notes before go-live

Always test on the merchant's actual production store

Most stores handle ?store= fine (verified on ProMap's demo store plus a merchant store). However, some app builds/configs fail: when the URL has a query/hash, the Google Maps script is not injected and the widget hangs on the spinner. Test directly on the target store before committing.

Fallback if the target store hangs with a param

Strip the param before the app initialises: add an inline script in the theme <head> (running before the app embed) that reads ?store= → saves it to sessionStorage → calls history.replaceState to a clean URL; the detail block then reads the id from sessionStorage. (Stripping after load does not work — the app already skipped loading Maps during page load.)

Matching the list item

The code matches the list item by store name (then falls back to index). Reason: if the app has autogeocode/distance sorting enabled, the list order (data-markerid) may differ from the data order, so matching by name is safer.

FAQ

Do I need a separate Google Maps API key?

No. The block reuses window.google.maps already loaded by the app (the same Store Locator key). The map renders on any domain where the app's own map already works.

What about large datasets (thousands of stores)?

In that case the app serves data via SCASLSetting.locationsUrl (a JSON file on a CDN) instead of inline locationsRaw. The code already includes a fetch(locationsUrl) fallback branch.

Can the?store=link be shared / bookmarked?

Yes — it's a normal deep-link. Just note it isn't an SEO URL (Google won't index it as a separate page).

What if the app ships an update that changes the data structure?

This block relies on internal structures (SCASLSetting, the .bh-sl-* classes), so it may need adjusting. The block is maintained by the merchant's team.

Store Locator (ProMap) — Developer guide

Did this answer your question?