Skip to content

    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.

    Installing on your site

    View source

    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.

    <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.

    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 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:

    1. 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.
    2. Checks it is not too late. If the page has already painted, it does nothing: it never hides something the shopper has seen.
    3. 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.
    4. 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 own nonce, so a nonce-based CSP allows it.
    5. Hands over, or gives up. Luma, once loaded, takes over the hold through window.__lbx.early and 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>.

    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.

    HintWhy
    The CDN, twiceThe 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 crossoriginLuma’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.

    With a CSP, allow:

    DirectiveValue
    script-srchttps://cdn.<region>.luigisbox.ai, and the snippet’s tag — give it the page’s nonce, or allow its hash
    connect-srchttps://api.<region>.luigisbox.ai, https://auth.<region>.luigisbox.ai
    worker-srcblob: — optional. Without it, the browser-token challenge is solved on the main thread, which is slower.
    trusted-typeslit-html, when Trusted Types are enforced

    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.

    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.

    For one browser sessionAppend ?_lbx_env=stop to any page. See Previews and releases.
    For everyoneSet disabled: true in _global/_config.ts and release, or remove the tags.