Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.
Everything goes in your site’s HTML, in <head>, on every page. The loader’s URL is the
repository’s loader_script_url — read it from the
repository record or copy it in
the Hub. It never changes.
Luma reads the publishable key and the channel from the integration’s configuration, not from the tag. See The integration folder.
The minimum: the loader alone
Section titled “The minimum: the loader alone”<script async src="https://cdn.eu1.luigisbox.ai/b/lbdr_8w3k2p/loader.js"></script>This works from a tag manager. On a hard page load, your own listing can show for a moment before Luma’s replaces it.
Recommended: hints, snippet, loader
Section titled “Recommended: hints, snippet, loader”The whole start of <head>, in this order, before your own stylesheets and scripts:
<head> <meta charset="utf-8"> <link rel="preconnect" href="https://cdn.eu1.luigisbox.ai"> <link rel="preconnect" href="https://cdn.eu1.luigisbox.ai" crossorigin> <link rel="preconnect" href="https://api.eu1.luigisbox.ai" crossorigin> <link rel="preconnect" href="https://auth.eu1.luigisbox.ai" crossorigin> <script>"use strict";(function(){try{if(window.__lbx&&window.__lbx.early)return;var n=JSON.parse(localStorage.getItem("lbx:early")||"null");if(!n||n.v!==1||!(n.exp>Date.now())||performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint").length)return;for(var o=new URL(location.href),i=0;i<n.routes.length;i++){var e=n.routes[i];if(new RegExp(e.path).test(o.pathname)){for(var r=!0,l=0;e.need&&l<e.need.length;l++)(o.searchParams.get(e.need[l])||"").trim()||(r=!1);if(r&&e.allow){var d=new RegExp(e.allow);o.searchParams.forEach(function(a,p){a&&!d.test(p)&&(r=!1)})}if(r){var t=document.createElement("style"),c=document.currentScript;c&&c.nonce&&(t.nonce=c.nonce),t.setAttribute("data-lbx","early"),t.textContent=e.css,(document.head||document.documentElement).appendChild(t);var f=function(){clearTimeout(u),t.parentNode&&t.parentNode.removeChild(t)},u=setTimeout(f,n.ms||1500),m=window.__lbx=window.__lbx||{};m.early={t:performance.now(),route:e.id,snippet:1,claim:function(a){return!t.parentNode||a!==e.id?!1:(clearTimeout(u),!0)},drop:f};return}}}}catch(a){}})();</script> <script async fetchpriority="high" src="https://cdn.eu1.luigisbox.ai/b/lbdr_8w3k2p/loader.js"></script> <!-- your own stylesheets and scripts -->Not through a tag manager: by the time it runs, the page has painted.
The early-hold snippet
Section titled “The early-hold snippet”The inline <script> in the block above. It hides your own listing before first paint, so
Luma’s replaces it with no flash. It is the same for every site and holds no settings of
yours: paste it as shown. It also ships with Luma, at
node_modules/@luigisbox/luma/snippet/early-hold.min.js in your repository; after a Luma
update, compare the two.
Unminified, it reads:
(function () { try { if (window.__lbx && window.__lbx.early) return; var r = JSON.parse(localStorage.getItem('lbx:early') || 'null'); if (!r || r.v !== 1 || !(r.exp > Date.now())) return; // Moved or injected late: never hide what the visitor has already seen. if (performance.getEntriesByName && performance.getEntriesByName('first-contentful-paint').length) return; var u = new URL(location.href); for (var i = 0; i < r.routes.length; i++) { var x = r.routes[i]; if (!new RegExp(x.path).test(u.pathname)) continue; var ok = true; for (var j = 0; x.need && j < x.need.length; j++) if (!(u.searchParams.get(x.need[j]) || '').trim()) ok = false; if (ok && x.allow) { var a = new RegExp(x.allow); u.searchParams.forEach(function (v, n) { if (v && !a.test(n)) ok = false; }); } if (!ok) continue; var s = document.createElement('style'); var me = document.currentScript; if (me && me.nonce) s.nonce = me.nonce; s.setAttribute('data-lbx', 'early'); s.textContent = x.css; (document.head || document.documentElement).appendChild(s); var drop = function () { clearTimeout(t); if (s.parentNode) s.parentNode.removeChild(s); }; var t = setTimeout(drop, r.ms || 1500); var L = (window.__lbx = window.__lbx || {}); L.early = { t: performance.now(), route: x.id, snippet: 1, claim: function (id) { if (!s.parentNode || id !== x.id) return false; clearTimeout(t); return true; }, drop: drop, }; return; } } catch (e) {}})();What it does, in order:
- Reads the rules. On earlier page views, Luma stored in
localStorage(lbx:early) which of your pages it takes over and the CSS that hides your listing on each. Without rules — a browser’s first page view, rules older than their expiry — it does nothing. - Checks it is not too late. If the page has already painted, it does nothing: it never hides something the shopper has seen.
- Matches the URL. A rule applies when the page’s path matches its pattern and the query parameters it needs are present — a search page needs its query. On a category page, every non-empty parameter must also be one Luma handles; a URL with any other is left alone.
- Hides your listing. It adds one
<style data-lbx="early">with that rule’s CSS, which hides your listing while keeping its space and shows a loading bar. It carries the snippet’s ownnonce, so a nonce-based CSP allows it. - Hands over, or gives up. Luma, once loaded, takes over the hold through
window.__lbx.earlyand shows its listing. If Luma does not claim it within the stored timeout — 1.5 seconds unless the rules say otherwise — the style is removed and your page shows as it would without Luma.
It makes no request, never blocks the page and never throws: any error ends it silently.
On a site whose React hydrates the whole document (Remix, React Router 7, Hydrogen), put the
loader first and the snippet as the last element of <head>.
Connection hints
Section titled “Connection hints”The preconnects open the connections Luma needs while the HTML is still arriving. On a first visit they save one to three round trips per host.
| Hint | Why |
|---|---|
| The CDN, twice | The loader is a classic script and the widget code loads as modules with CORS. A browser does not share a connection between the two modes. Drop the plain one if the Network panel shows a single connection. |
The API and auth hosts, with crossorigin | Luma’s requests are CORS fetch() calls. A preconnect without crossorigin opens a connection they cannot use. |
Use the region of your repository — eu1 above. Do not add more than these four: extra
preconnects compete with your page’s own connections, and an unused one closes after about
10 seconds.
fetchpriority="high" on the loader moves it ahead of the page’s images, which an async
script otherwise follows. Use it on search and category pages, where Luma’s listing is the
main content, and measure it with yarn luma footprint
before keeping it.
Content Security Policy
Section titled “Content Security Policy”With a CSP, allow:
| Directive | Value |
|---|---|
script-src | https://cdn.<region>.luigisbox.ai, and the snippet’s tag — give it the page’s nonce, or allow its hash |
connect-src | https://api.<region>.luigisbox.ai, https://auth.<region>.luigisbox.ai |
worker-src | blob: — optional. Without it, the browser-token challenge is solved on the main thread, which is slower. |
trusted-types | lit-html, when Trusted Types are enforced |
HTTPS only
Section titled “HTTPS only”Luma does not start on a plain-HTTP page. The site keeps its own search, listings and
recommendations, and the browser console says why. localhost counts as secure in
development.
Server-rendered listings
Section titled “Server-rendered listings”Search and category listings can also arrive in your page’s HTML, rendered by Luigi’s Box and included by your own web server or CDN — nginx SSI or Varnish and CDN ESI. The page then has results at first paint, including for clients that do not run JavaScript, and your own listing remains the fallback.
This needs an include at your listing and a short addition to your server configuration. Luigi’s Box provides both for your site; ask your contact. The include must sit outside any part of the page your own frontend framework hydrates. Recommenders always render in the browser.
Turning it off
Section titled “Turning it off”| For one browser session | Append ?_lbx_env=stop to any page. See Previews and releases. |
| For everyone | Set disabled: true in _global/_config.ts and release, or remove the tags. |
See also
Section titled “See also”- Behaviour and fallbacks — what happens when something fails
- Visitor data and analytics — the cookie and storage Luma uses
- Analytics — the events Luma sends
- Browser tokens — the exchange Luma performs with your publishable key
Was this page helpful?
Thanks.