[object Object]

← back to Dw Theme Boost Fix

Fix new-arrivals scroll-jump: deep ?page=N->paged + anti-collapse image-box CSS (CLS 1.9->0.12 on page 7)

04227c1332e345c39210a7b30c00801dc3205d8a · 2026-08-04 10:29:31 -0700 · Steve Abrams

Files touched

Diff

commit 04227c1332e345c39210a7b30c00801dc3205d8a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 4 10:29:31 2026 -0700

    Fix new-arrivals scroll-jump: deep ?page=N->paged + anti-collapse image-box CSS (CLS 1.9->0.12 on page 7)
---
 boost-infinite-override.liquid | 384 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 384 insertions(+)

diff --git a/boost-infinite-override.liquid b/boost-infinite-override.liquid
new file mode 100644
index 0000000..d27b381
--- /dev/null
+++ b/boost-infinite-override.liquid
@@ -0,0 +1,384 @@
+{% comment %}
+  ============================================================================
+  Boost Infinite-Scroll Override  (DW, 2026-06-24)
+  ----------------------------------------------------------------------------
+  Boost AI Search & Discovery exposes NO API to flip pagination to infinite
+  scroll, and the account-level dashboard setting serves
+  paginationType:"default" (numbered) to the collection/search surfaces.
+
+  This snippet forces infinite scroll by rewriting Boost's own runtime config
+  object (window.boostSDAppConfig) BEFORE Boost's lazy bundle reads it.
+
+  TIMING — this MUST render in <head> IMMEDIATELY BEFORE {{ content_for_header }}.
+  Boost injects boostSDAppConfig via content_for_header app-embed scripts, so a
+  defineProperty setter installed *before* that injection intercepts the moment
+  Boost assigns the global. A theme asset (boost-sd-custom.js) loads on Boost's
+  own schedule and can run AFTER Boost has already read the config, so the
+  config-patch cannot live there — only the belt-and-suspenders fallbacks do.
+
+  Strategy (belt + suspenders):
+    1. defineProperty setter on window.boostSDAppConfig — patches on assignment.
+    2. If already defined (re-entry / SPA nav), patch immediately.
+    3. Short rAF/interval poll re-applies in case Boost replaces the object.
+    4. Patch every pagination knob found: generalSettings.* and
+       additionalElements.pagination.* across all surfaces.
+
+  ----------------------------------------------------------------------------
+  ENDPOINT REPOINT  (DW, 2026-07-13 — customer-facing regression fix)
+  Boost's app-embed config snippet ("app snippet: config", rendered before
+  </body> via the boost-sd app embed in settings_data.json) hardcodes the
+  boostSDAppConfig.api URLs to https://staging.bc-solutions.net/... which
+  returns 403 on every call → empty filter trees, dead search page, broken
+  infinite scroll. The snippet has a production self-correction block keyed on
+  themeInfo env, but at runtime the staging values still win (the final
+  `merge(boostSDAppConfig, window.boostSDAppConfig)` lets a pre-existing
+  window config's keys — populated with the staging api — take precedence).
+  We cannot edit the app embed's source, so patchApi() below rewrites any
+  staging bc-solutions endpoint to the VERIFIED production values every time
+  the config passes through this interceptor. TARGETED: only values containing
+  'staging.bc-solutions.net' / 'boost-cdn-staging' are rewritten, so if Boost
+  fixes this upstream the patch becomes a no-op. Production values verified
+  2026-07-13 against services.mybcapps.com (filter: 75,278 products;
+  search 'grasscloth': 8,203; suggest 'milano': 85) and against Boost's own
+  env==='production' Object.assign block in the same app snippet
+  (recommendUrl /discovery/recommend, analyticsUrl lambda.mybcapps.com/e,
+  cdn boost-cdn-prod.bc-solutions.net).
+  ============================================================================
+{% endcomment %}
+<script>
+/* DW-GUARD-VERSION 20260804-1030 */
+(function () {
+  'use strict';
+  if (window.__dwBoostInfinite) return;
+  window.__dwBoostInfinite = true;
+
+
+  /* DW Boost duplicate-request suppressor (v2 — buffered, body-safe).
+     Landing on a deep-linked ?page=N (N>1) in infinite-scroll mode makes Boost
+     fire its bc-sf-filter/search request TWICE (identical params + identical t=).
+     We can't edit Boost's bundle, so we coalesce: the 1st call hits the network,
+     its body is buffered ONCE, and any identical 2nd call within a short window is
+     served a fresh Response built from that buffer — so both Boost init paths get
+     VALID JSON (no empty-clone crash), from ONE round-trip, resolving together =
+     one clean render. page= is part of the key, so real scroll-appends are never
+     suppressed. Idempotent. */
+  (function () {
+    'use strict';
+    if (window.__dwBoostDedup) return;
+    window.__dwBoostDedup = true;
+
+    // 5s: the real-browser double-fire is Boost re-firing the SAME page (often a
+    // page=1 reset on a deep-linked landing) ~3.3s apart, well past a 1.5s guard.
+    // Distinct pages keep distinct keys, so a wide window never touches real
+    // scroll-appends — only an identical same-page/same-filter repeat is collapsed.
+    var WINDOW_MS = 5000;
+    var recent = Object.create(null);           // key -> { ts, buf: Promise<{body,init}|null> }
+
+    function isSearch(u) { return /bc-sf-filter\/(search|filter)/i.test(String(u)); }
+    function keyOf(u) {
+      try { return String(u).replace(/([?&])t=\d+/, '$1t=').replace(/([?&])event=[^&]*/, '$1event='); }
+      catch (e) { return String(u); }
+    }
+    function fresh(k) { var h = recent[k]; return h && (Date.now() - h.ts) < WINDOW_MS ? h : null; }
+
+    var origFetch = window.fetch;
+    if (typeof origFetch !== 'function') return;
+
+    window.fetch = function (input, init) {
+      var url = (typeof input === 'string') ? input : (input && input.url) || '';
+      if (!isSearch(url)) return origFetch.call(this, input, init);
+
+      var k = keyOf(url), h = fresh(k);
+      if (h && h.buf) {
+        // Duplicate within window: serve a fresh, fully-readable Response from the
+        // buffered bytes. If buffering failed, fall back to a real network fetch.
+        var self = this;
+        return h.buf.then(function (o) {
+          if (!o) return origFetch.call(self, input, init);
+          return new Response(o.body ? o.body.slice(0) : null, o.init);
+        });
+      }
+
+      // First (real) call: fetch, then clone+buffer the body the instant it
+      // resolves (before any consumer reads it), so duplicates get valid data.
+      var real = origFetch.call(this, input, init);
+      recent[k] = {
+        ts: Date.now(),
+        buf: real.then(function (resp) {
+          return resp.clone().arrayBuffer().then(function (ab) {
+            return { body: ab, init: { status: resp.status, statusText: resp.statusText || '', headers: resp.headers } };
+          });
+        }).catch(function () { return null; })
+      };
+      recent[k].buf.then(function () { if (recent[k]) recent[k].ts = Date.now(); });
+      return real;                               // first caller gets the untouched original
+    };
+  })();
+
+
+
+  /* DW same-origin request governor + 429/430 auto-backoff (TK-10074, 2026-07-30).
+     Reproduced: Shopify local_rate_limited trips ~30-45 origin req/s per IP, ~32s
+     lockout. The theme is request-heavy (~1700 req/session; infinite scroll cascades
+     ~14 pages in <8s), so a bursty visitor (fast nav / prefetch / multi-tab / dense
+     grid) can self-inflict 429 and see the raw local_rate_limit page. Installed AFTER
+     the dedup wrapper so it is the OUTERMOST window.fetch: caps concurrent same-origin
+     requests to 6 (can't reach the trip point) AND retries 429/430 invisibly honoring
+     Retry-After, so the grid just pauses instead of erroring. Idempotent. */
+  (function(){
+    'use strict';
+    if (window.__dwReqGovernor) return; window.__dwReqGovernor = true;
+    var ORIGIN = location.origin, MAX_CONCURRENT = 6, active = 0, queue = [];
+    var origFetch = window.fetch;              // wraps whatever is current (incl. dedup)
+    if (typeof origFetch !== 'function') return;
+    function pump(){ while (active < MAX_CONCURRENT && queue.length) queue.shift()(); }
+    function isOrigin(u){ try { u = String(u); return u.indexOf(ORIGIN) === 0 || u.charAt(0) === '/'; } catch(e){ return false; } }
+    window.fetch = function(input, init){
+      var url = (typeof input === 'string') ? input : (input && input.url) || '';
+      if (!isOrigin(url)) return origFetch.call(this, input, init);
+      var self = this;
+      return new Promise(function(resolve, reject){
+        queue.push(function run(){
+          active++;
+          (function attempt(tries){
+            origFetch.call(self, input, init).then(function(resp){
+              if ((resp.status === 429 || resp.status === 430) && tries < 4){
+                var ra = parseFloat(resp.headers.get('retry-after'));
+                var wait = (isFinite(ra) && ra > 0 ? ra*1000 : Math.min(1000*Math.pow(2,tries), 8000)) + Math.random()*400;
+                setTimeout(function(){ attempt(tries + 1); }, wait); return;
+              }
+              active--; pump(); resolve(resp);
+            }).catch(function(e){ active--; pump(); reject(e); });
+          })(0);
+        });
+        pump();
+      });
+    };
+  })();
+
+  // USER PAGINATION CHOICE (DW, 2026-07-13 — Steve's verdict on customer
+  // pushback): infinite scroll stays the DEFAULT, but the shopper can switch
+  // to classic numbered pages of 100 via the floating toggle rendered below.
+  // Choice persists in localStorage and survives reloads/navigation.
+  var PAGED_LIMIT = 100;
+  var mode = 'infinite';
+  try { if (localStorage.getItem('dwPaginationMode') === 'paged') mode = 'paged'; } catch (e) {}
+
+  // DEEP-LANDING -> PAGED (DW, 2026-08-04 — residual scroll-jump fix, Steve-approved).
+  // A deep-linked ?page=N (N>1) has no stable anchor in infinite scroll, and its
+  // batch-append near the fold shoves the footer/main (~0.8 CLS = "images jump").
+  // Rendering deep landings as classic paged (100/page) has ZERO append shove.
+  // NOT persisted -> a normal landing (no page param) keeps infinite as the default
+  // per Steve's 2026-07-13 verdict; only genuine deep ?page=N links go paged.
+  if (mode === 'infinite') {
+    try { if (parseInt(new URL(location.href).searchParams.get('page'), 10) > 1) mode = 'paged'; } catch (e) {}
+  }
+
+  var TARGET = mode === 'paged' ? 'default' : 'infinite_scroll';
+
+  // DEEP-LINK NORMALIZE (DW, 2026-07-28 — root fix for the deep-page jump/flip):
+  // Infinite scroll has no real "page N" anchor. Landing on ?page=N (N>1) makes
+  // Boost reconcile deep — resetting to page 1, double-firing its search, AND
+  // cascading many unscrolled auto-loads (the "really bad", worse-the-deeper
+  // jump). In infinite mode we strip the page param BEFORE Boost reads the URL,
+  // so it starts clean at page 1 and only appends on real scroll. Paged mode
+  // (numbered pages of 100) keeps page=N — it's meaningful there.
+  if (mode !== 'paged') {
+    try {
+      var _u = new URL(location.href);
+      if (parseInt(_u.searchParams.get('page'), 10) > 1) {
+        _u.searchParams.delete('page');
+        history.replaceState(null, '', _u.pathname + _u.search + _u.hash);
+      }
+    } catch (e) {}
+  }
+
+  // CASCADE GUARD (DW, 2026-07-30 — kills the intermittent deep-page runaway).
+  // Even with page= stripped, a deep-link landing intermittently (a real-browser
+  // timing race, ~1/3, invisible to headless) jumps the viewport down ~3000px on
+  // its own; that puts Boost's scroll sentinel on-screen and fires a BURST of
+  // auto-appends (24 -> 72+ cards) before the shopper has touched anything — the
+  // "nightmare". The URL strip alone doesn't cover it because the trigger is a
+  // native scroll jump, not a stale page param. So in infinite mode we (1) take
+  // scroll restoration off automatic, and (2) pin the viewport to the top until a
+  // GENUINE user gesture, so no second page can auto-load on landing. The instant
+  // the shopper scrolls/wheels/touches, the guard releases and normal infinite
+  // scroll resumes. Paged mode is untouched.
+  if (mode !== 'paged') {
+    try { if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; } catch (e) {}
+    window.__dwUserEngaged = false;
+    var release = function () {
+      if (window.__dwUserEngaged) return;
+      window.__dwUserEngaged = true;
+      ['wheel', 'touchstart', 'keydown', 'pointerdown', 'mousedown'].forEach(function (evt) {
+        try { window.removeEventListener(evt, release, true); } catch (e) {}
+      });
+    };
+    ['wheel', 'touchstart', 'keydown', 'pointerdown', 'mousedown'].forEach(function (evt) {
+      try { window.addEventListener(evt, release, { capture: true, passive: true }); } catch (e) {}
+    });
+    // Hold the top for a short landing window; stop the instant the user engages
+    // or the window elapses, so we never fight a shopper who scrolls immediately.
+    var pinStart = Date.now();
+    var pin = function () {
+      if (window.__dwUserEngaged) return;                 // user took over -> stop
+      if (Date.now() - pinStart > 4000) return;           // safety cap -> stop
+      try { if (window.pageYOffset > 0) window.scrollTo(0, 0); } catch (e) {}
+      (window.requestAnimationFrame || window.setTimeout)(pin, 16);
+    };
+    try {
+      if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', pin);
+      else pin();
+    } catch (e) {}
+  }
+
+  // Verified production Boost SD endpoints for this shop (2026-07-13).
+  var PROD_API = {
+    filterUrl:     'https://services.mybcapps.com/bc-sf-filter/filter',
+    searchUrl:     'https://services.mybcapps.com/bc-sf-filter/search',
+    suggestionUrl: 'https://services.mybcapps.com/bc-sf-filter/search/suggest',
+    productsUrl:   'https://services.mybcapps.com/bc-sf-filter/search/products',
+    recommendUrl:  'https://services.mybcapps.com/discovery/recommend',
+    analyticsUrl:  'https://lambda.mybcapps.com/e',
+    cdn:           'https://boost-cdn-prod.bc-solutions.net'
+  };
+
+  // Rewrite ONLY staging bc-solutions endpoints to production. Anything that
+  // already points at a non-staging host is left untouched, so an upstream
+  // Boost fix (or an intentional custom endpoint) is never clobbered.
+  function isStaging(v) {
+    return typeof v === 'string' &&
+      (v.indexOf('staging.bc-solutions.net') !== -1 ||
+       v.indexOf('boost-cdn-staging') !== -1);
+  }
+  function patchApi(api) {
+    if (!api || typeof api !== 'object') return;
+    for (var k in PROD_API) {
+      if (!Object.prototype.hasOwnProperty.call(PROD_API, k)) continue;
+      try {
+        if (isStaging(api[k])) api[k] = PROD_API[k];
+      } catch (e) {}
+    }
+  }
+
+  // Recursively rewrite any "paginationType" key to infinite_scroll, plus the
+  // common Boost shapes (generalSettings + additionalElements.pagination).
+  // `seen` guards against cyclic / shared references so a self-referential
+  // Boost config can't loop or get re-walked needlessly.
+  function patchObj(o, depth, seen) {
+    if (!o || typeof o !== 'object' || depth > 6) return;
+    if (seen.indexOf(o) !== -1) return;
+    seen.push(o);
+    try {
+      if (Object.prototype.hasOwnProperty.call(o, 'paginationType') &&
+          typeof o.paginationType === 'string') {
+        o.paginationType = TARGET;
+      }
+      // Boost sometimes nests under .pagination: { paginationType, ... }
+      if (o.pagination && typeof o.pagination === 'object' &&
+          typeof o.pagination.paginationType === 'string') {
+        o.pagination.paginationType = TARGET;
+      }
+      // Paged mode: bump the grid's per-page product count to 100. Only keys
+      // that sit next to a pagination knob are touched — searchPanelBlocks'
+      // pageSize (the instant-search dropdown, 25 items) must stay untouched.
+      if (mode === 'paged' &&
+          (Object.prototype.hasOwnProperty.call(o, 'paginationType') ||
+           (o.pagination && typeof o.pagination === 'object'))) {
+        ['limit', 'productPerPage', 'productsPerPage'].forEach(function (k) {
+          if (typeof o[k] === 'number' && o[k] > 0 && o[k] < PAGED_LIMIT) o[k] = PAGED_LIMIT;
+        });
+      }
+    } catch (e) {}
+    for (var k in o) {
+      if (!Object.prototype.hasOwnProperty.call(o, k)) continue;
+      var v = o[k];
+      if (v && typeof v === 'object') patchObj(v, depth + 1, seen);
+    }
+  }
+
+  function patchConfig(cfg) {
+    if (!cfg || typeof cfg !== 'object') return cfg;
+    try {
+      // One shared `seen` set across all walks keeps total work O(n) and
+      // cycle-safe — known surfaces get a full-depth targeted walk, then the
+      // catch-all root walk skips anything already visited (schema-drift safe).
+      var seen = [];
+      patchApi(cfg.api); // endpoint repoint — staging → production (2026-07-13)
+      if (cfg.generalSettings) patchObj(cfg.generalSettings, 0, seen);
+      if (cfg.additionalElements) patchObj(cfg.additionalElements, 0, seen);
+      patchObj(cfg, 0, seen);
+    } catch (e) {}
+    return cfg;
+  }
+
+  // (1) defineProperty setter — intercept Boost's assignment.
+  var _cfg = window.boostSDAppConfig;
+  try {
+    Object.defineProperty(window, 'boostSDAppConfig', {
+      configurable: true,
+      enumerable: true,
+      get: function () { return _cfg; },
+      set: function (val) { _cfg = patchConfig(val); }
+    });
+  } catch (e) {
+    // If defineProperty fails (already non-configurable), fall through to poll.
+  }
+
+  // (2) Patch immediately if it already exists.
+  if (_cfg) patchConfig(_cfg);
+
+  // (3) Short poll — re-apply in case Boost replaces / re-inits the object,
+  // and re-patch after a few frames so the value is correct at bundle init.
+  var ticks = 0;
+  var iv = setInterval(function () {
+    ticks++;
+    try { if (window.boostSDAppConfig) patchConfig(window.boostSDAppConfig); } catch (e) {}
+    if (ticks > 80) clearInterval(iv); // ~8s @ 100ms, well past Boost init
+  }, 100);
+
+  {% if request.page_type == 'collection' or request.page_type == 'search' %}
+  // Floating browse-mode toggle (collection + search grids only).
+  document.addEventListener('DOMContentLoaded', function () {
+    if (document.getElementById('dw-page-mode')) return;
+    var wrap = document.createElement('div');
+    wrap.id = 'dw-page-mode';
+    wrap.setAttribute('role', 'group');
+    wrap.setAttribute('aria-label', 'Browsing mode');
+    wrap.style.cssText = 'position:fixed;bottom:18px;left:18px;z-index:9990;display:flex;' +
+      'background:#fff;border:1px solid #d8d4cc;border-radius:999px;box-shadow:0 2px 10px rgba(0,0,0,.12);' +
+      'font:12px/1 -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;overflow:hidden';
+    function btn(label, m, active) {
+      var b = document.createElement('button');
+      b.type = 'button';
+      b.textContent = label;
+      b.setAttribute('aria-pressed', active ? 'true' : 'false');
+      b.style.cssText = 'border:0;cursor:pointer;padding:9px 14px;letter-spacing:.04em;' +
+        (active ? 'background:#1a1a1a;color:#fff;' : 'background:transparent;color:#444;');
+      b.addEventListener('click', function () {
+        if (mode === m) return;
+        try { localStorage.setItem('dwPaginationMode', m); } catch (e) {}
+        location.reload();
+      });
+      return b;
+    }
+    wrap.appendChild(btn('Endless scroll', 'infinite', mode === 'infinite'));
+    wrap.appendChild(btn('Pages of 100', 'paged', mode === 'paged'));
+    document.body.appendChild(wrap);
+  });
+  {% endif %}
+})();
+</script>
+{% if request.page_type == 'collection' or request.page_type == 'search' %}
+<style id="dw-anti-collapse">
+/* DW 2026-08-04: reserve each Boost card's image box from inject-time (up the full
+   a > div > wrapper chain) so lazy images can't reflow siblings on infinite-scroll
+   append. Matches the theme's existing forced 1/1 card look (no visual change). */
+.boost-sd__product-item-grid-view-layout-image,
+.boost-sd__product-link-image,
+.boost-sd__product-link-image > div{ aspect-ratio:1/1 !important; overflow:hidden; display:block; }
+.boost-sd__product-image-wrapper,
+.boost-sd__product-image{ aspect-ratio:1/1 !important; overflow:hidden; }
+.boost-sd__product-image-img{ width:100% !important; height:100% !important; object-fit:cover !important; }
+</style>
+{% endif %}

← 62e4c65 fix: cascade-guard for deep-page infinite-scroll runaway (Ho  ·  back to Dw Theme Boost Fix  ·  TK-10629: de-leak Versa Designed Surfaces -> Hollywood Contr 2e45e29 →