PageSourceSearch

https://vertisourcehr.com/wp-content/themes/vshr-sage/public/blog-hub.js?v=1790314188

js vertisourcehr.com collected 2026-10-04 03:32:04 UTC 21,728 bytes, 309 lines download raw bytes

1/* blog-hub.js — VSHR Knowledge Hub interactive explorer.
2   Progressive enhancement over the server-rendered index: instant client-side
3   search, topic filtering, and a compliance-by-state choropleth (state
4   geometry reused from the Minimum Wage Navigator, Albers USA 1050x600).
5   The server-rendered featured + 12-card grid stay as the JS-off fallback;
6   this script takes over #bxResults once the post index has loaded. */
7(function () {
8  'use strict';
9  var root = document.getElementById('bxHub');
10  if (!root) return;
11
12  var REST = root.dataset.rest;                 // /wp-json/wp/v2
13  var GEO  = root.dataset.geo;                  // blog-map-geo.json url
14  var FEATURED = parseInt(root.dataset.featured || '0', 10) || 0; // lead-story post ID (kept out of the default grid)
15  var HIDE = root.dataset.hideCat || '';         // category slug kept out of the unfiltered grid (Compliance Wire), mirrors the server grid
16  var elMap    = document.getElementById('bxMap');
17  var elSearch = document.getElementById('bxSearch');
18  var elChips  = document.getElementById('bxChips');
19  var elResults= document.getElementById('bxResults');
20  var elCount  = document.getElementById('bxCount');
21  var elActive = document.getElementById('bxActive');
22  var elMore   = document.getElementById('bxMore');
23
24  /* ---- category → topic icon + accent (mirrors partials/blog/thumb.blade.php) ---- */
25  function themeFor(slug) {
26    var g = [
27      [['legal-news-compliance','compliance','anti-harassment','wage-hour','independent-contractor','freedom-from-workplace-bullies-week'], 'legal', '#29ABE2'],
28      [['california-compliance','multi-state-compliance'], 'map', '#34D3B5'],
29      [['minimum-wage','payroll','outsourcing-accounting','wage'], 'coins', '#F9A03F'],
30      [['benefits-compensation','benefits-administration','employer-supported-childcare','insurance','workers-comp','workers-compensation-and-risk-managment'], 'shield', '#3DD68C'],
31      [['employee-wellness','healthcare','covid-19-coronavirus'], 'leaf', '#2DD4BF'],
32      [['training-workplace-culture','workplace-culture','leadership-management'], 'growth', '#A78BFA'],
33      [['employee-retention','hiring','employee-onboarding'], 'badge', '#38BDF8'],
34      [['workplace-safety'], 'safety', '#FBBF24'],
35      [['human-resources','human-resource-management'], 'nodes', '#5B9BE0'],
36      [['hris','hr-technology','hr-software','artificial-intelligence'], 'chip', '#818CF8'],
37      [['timekeeping'], 'clock', '#29ABE2']
38    ];
39    for (var i = 0; i < g.length; i++) if (g[i][0].indexOf(slug) !== -1) return { icon: g[i][1], accent: g[i][2] };
40    return { icon: 'mark', accent: '#29ABE2' };
41  }
42  var ICONS = {
43    legal:'<path class="dt-fill" d="M5 8l-2.6 5h5.2L5 8zm14 0l-2.6 5h5.2L19 8z"/><path class="dt-line" d="M12 3v18M7.5 21h9M12 6 5 8m7-2 7 2M5 8l-2.6 5h5.2L5 8zm14 0l-2.6 5h5.2L19 8z"/><path class="dt-line" d="M2.4 13a2.6 2.6 0 0 0 5.2 0M16.4 13a2.6 2.6 0 0 0 5.2 0"/>',
44    map:'<path class="dt-fill" d="M12 2c4 0 7 3 7 6.9 0 4.7-7 12.1-7 12.1S5 13.6 5 8.9C5 5 8 2 12 2z"/><path class="dt-line" d="M12 2c4 0 7 3 7 6.9 0 4.7-7 12.1-7 12.1S5 13.6 5 8.9C5 5 8 2 12 2z"/><circle class="dt-line" cx="12" cy="9" r="2.4"/>',
45    coins:'<ellipse class="dt-fill" cx="12" cy="7" rx="7" ry="3"/><path class="dt-line" d="M5 7c0 1.7 3.1 3 7 3s7-1.3 7-3-3.1-3-7-3-7 1.3-7 3z"/><path class="dt-line" d="M5 7v5c0 1.7 3.1 3 7 3s7-1.3 7-3V7"/><path class="dt-line" d="M5 12v5c0 1.7 3.1 3 7 3s7-1.3 7-3v-5"/>',
46    shield:'<path class="dt-fill" d="M12 2l8 3v6c0 5-3.5 8.6-8 11-4.5-2.4-8-6-8-11V5l8-3z"/><path class="dt-line" d="M12 2l8 3v6c0 5-3.5 8.6-8 11-4.5-2.4-8-6-8-11V5l8-3z"/><path class="dt-line" d="M12 8v6M9 11h6"/>',
47    leaf:'<path class="dt-fill" d="M4 20c0-9 7-16 16-16 0 9-7 16-16 16z"/><path class="dt-line" d="M4 20c0-9 7-16 16-16 0 9-7 16-16 16z"/><path class="dt-line" d="M4.5 19.5C8 14 12 11 17 9"/>',
48    growth:'<path class="dt-fill" d="M4 14h4v6H4zm6-3h4v9h-4zm6-3h4v12h-4z"/><path class="dt-line" d="M4 14h4v6H4zm6-3h4v9h-4zm6-3h4v12h-4z"/><path class="dt-line" d="M4 10l6-4 4 2 6-4M16 4h4v4"/>',
49    badge:'<circle class="dt-fill" cx="12" cy="9.5" r="6.5"/><circle class="dt-line" cx="12" cy="9.5" r="6.5"/><path class="dt-line" d="M9.2 9.6l2 2 3.6-3.7"/><path class="dt-line" d="M8.5 15.4 7 21l5-2 5 2-1.5-5.6"/>',
50    safety:'<path class="dt-fill" d="M3.5 17a8.5 8.5 0 0 1 17 0z"/><path class="dt-line" d="M3.5 17a8.5 8.5 0 0 1 17 0"/><path class="dt-line" d="M2 17h20M9 8.5V6.5h6v2M12 8.5V5"/>',
51    nodes:'<circle class="dt-fill" cx="12" cy="5" r="2.6"/><circle class="dt-fill" cx="5" cy="19" r="2.6"/><circle class="dt-fill" cx="19" cy="19" r="2.6"/><circle class="dt-line" cx="12" cy="5" r="2.6"/><circle class="dt-line" cx="5" cy="19" r="2.6"/><circle class="dt-line" cx="19" cy="19" r="2.6"/><path class="dt-line" d="M12 7.6v3.9M12 11.5H5v4.9M12 11.5h7v4.9"/>',
52    chip:'<rect class="dt-fill" x="7" y="7" width="10" height="10" rx="1.5"/><rect class="dt-line" x="6" y="6" width="12" height="12" rx="2"/><rect class="dt-line" x="9.5" y="9.5" width="5" height="5" rx="1"/><path class="dt-line" d="M9 6V3m6 3V3M9 21v-3m6 3v-3M6 9H3m3 6H3m18-6h-3m3 6h-3"/>',
53    clock:'<circle class="dt-fill" cx="12" cy="12" r="9"/><circle class="dt-line" cx="12" cy="12" r="9"/><path class="dt-line" d="M12 7v5l3.5 2"/>',
54    mark:'<path class="dt-fill" d="M6 3h9l4 4v14l-6.5-3.1L6 21V3z"/><path class="dt-line" d="M6 3h9l4 4v14l-6.5-3.1L6 21z"/><path class="dt-line" d="M9 9h6M9 12.5h4"/>'
55  };
56
57  var NAME2AB = {Alabama:'AL',Alaska:'AK',Arizona:'AZ',Arkansas:'AR',California:'CA',Colorado:'CO',Connecticut:'CT',Delaware:'DE',Florida:'FL',Georgia:'GA',Hawaii:'HI',Idaho:'ID',Illinois:'IL',Indiana:'IN',Iowa:'IA',Kansas:'KS',Kentucky:'KY',Louisiana:'LA',Maine:'ME',Maryland:'MD',Massachusetts:'MA',Michigan:'MI',Minnesota:'MN',Mississippi:'MS',Missouri:'MO',Montana:'MT',Nebraska:'NE',Nevada:'NV','New Hampshire':'NH','New Jersey':'NJ','New Mexico':'NM','New York':'NY','North Carolina':'NC','North Dakota':'ND',Ohio:'OH',Oklahoma:'OK',Oregon:'OR',Pennsylvania:'PA','Rhode Island':'RI','South Carolina':'SC','South Dakota':'SD',Tennessee:'TN',Texas:'TX',Utah:'UT',Vermont:'VT',Virginia:'VA',Washington:'WA','West Virginia':'WV',Wisconsin:'WI',Wyoming:'WY','District of Columbia':'DC'};
58  var AB2NAME = {};
58 Object.keys(NAME2AB).forEach(function (n) { AB2NAME[NAME2AB[n]] = n; });
59  var STATE_NAMES = Object.keys(NAME2AB);
60
61  function decode(s){ var t = document.createElement('textarea'); t.innerHTML = s; return t.value.replace(/<[^>]+>/g, '').trim(); }
62  function esc(s){ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
63  /* word-safe truncation: cut at the last word boundary before max, real ellipsis */
64  function clip(s, max){
65    s = (s || '').trim();
66    if (s.length <= max) return s;
67    var cut = s.slice(0, max);
68    var sp = cut.lastIndexOf(' ');
69    if (sp > 0) cut = cut.slice(0, sp);
70    return cut.replace(/[\s.,;:!?–—…-]+$/, '') + '…';
71  }
72
73  var POSTS = [];            // full index
74  var CATS = {};             // id -> {name,slug}
75  var STATE_COUNT = {};      // ab -> count
76  var filter = { q: '', cat: '', state: '' };
77  var shown = 0, PAGE = 18;
78
79  /* ───────── data load ───────── */
80  /* SHAPE is the contract between a cached record and cardHTML(). It is baked
81     into the cache key AND re-checked on every read, so adding a field to the
82     record built below invalidates every existing cache by itself.
83
84     Bumping a hand-written key is what failed before: b0a4619d added `img` (the
85     real article image) but left the key at its previous value, so any tab
86     already holding a pre-fix payload kept reading p.img === undefined and
87     rendered all 182 cards as the branded icon panel. Nothing caught it — the
88     server-rendered fallback in the HTML was correct, the REST field was
89     correct, and the cache only dies when the tab does. */
90  var SHAPE = ['id', 't', 'u', 'e', 'c', 'cs', 'd', 'st', 'img', 'icon', 'accent'];
91  var CACHE_KEY = 'bxPosts:' + SHAPE.join(',');
92  try { ['bxPosts', 'bxPosts2', 'bxPosts3'].forEach(function (k) { sessionStorage.removeItem(k); }); } catch (e) {}
93
94  function readCache() {
95    var raw = null;
96    try { raw = JSON.parse(sessionStorage.getItem(CACHE_KEY) || 'null'); } catch (e) { return null; }
97    if (!raw || !raw.length) return null;
98    for (var i = 0; i < raw.length; i++) {
99      for (var k = 0; k < SHAPE.length; k++) {
100        if (!(SHAPE[k] in raw[i])) return null;   // predates a field cardHTML() reads
101      }
102    }
103    return raw;
104  }
105
106  /* Stale-while-revalidate. The shape check above is necessary but NOT
107     sufficient: on 2026-08-18 a tab cached a payload whose shape was current
108     but whose CONTENT predated a server-side change (config/hub-thumbs.php was
109     emptied minutes after the tab stored img:'' for 12 posts), and the tab
110     kept painting icon panels on every reload. No key scheme can see a content
111     change, so the cache is now only allowed to win the first paint: every
112     load refetches the index in the background and repaints if it differs. */
113  function loadPosts() {
114    var cached = readCache();
115    if (cached) { POSTS = cached; revalidate(); return Promise.resolve(); }
116    return fetchIndex().then(function (fresh) { POSTS = fresh; saveCache(); });
117  }
118  function saveCache() {
119    try { sessionStorage.setItem(CACHE_KEY, JSON.stringify(POSTS)); } catch (e) {}
120  }
121  function revalidate() {
122    fetchIndex().then(function (fresh) {
123      var changed = JSON.stringify(fresh) !== JSON.stringify(POSTS);
124      POSTS = fresh; saveCache();
125      if (changed) { buildChips(); render(false); }
126    }).catch(function (e) { /* offline etc. — cached paint stands */ });
127  }
128  function fetchIndex() {
129    return fetch(REST + '/categories?per_page=100&_fields=id,name,slug')
130      .then(function (r) { return r.json(); })
131      .then(function (cs) {
132        cs.forEach(function (c) { CATS[c.id] = { name: decode(c.name), slug: c.slug }; });
133        return fetchPage(1, []);
134      })
135      .then(function (all) {
136        return all.map(function (p) {
137          var title = decode(p.title.rendered);
138          // bx_excerpt = the server's word-safe cleaned excerpt (App\vshr_blog_excerpt,
139          // already plain text); fall back to the decoded rendered excerpt. clip()
140          // guarantees a word-boundary cut either way.
141          var ex = clip(p.bx_excerpt ? p.bx_excerpt : decode(p.excerpt.rendered), 150);
142          var hay = title + ' ' + ex;
143          var st = STATE_NAMES.filter(function (n) { return new RegExp('\\b' + n + '\\b', 'i').test(hay); }).map(function (n) { return NAME2AB[n]; });
144          // Mirrors App vshr_primary_cat(): 'Blog'/'Uncategorized' are container
145          // taxonomy, not editorial signal - skip unless nothing else exists.
146          var pc = { name: 'Insights', slug: '' }, pcFirst = null;
147          (p.categories || []).forEach(function (cid) {
148            var c = CATS[cid]; if (!c) return;
149            if (!pcFirst) pcFirst = c;
150            if (pc.slug === '' && c.slug !== 'blog' && c.slug !== 'uncategorized') pc = c;
151          });
152          // No pcFirst fallback: a bare 'Blog'/'Uncategorized' post reads as Insights.
153          var th = themeFor(pc.slug);
154          return { id: p.id, t: title, u: p.link.replace(/^https?:\/\/[^/]+/, ''), e: ex, c: pc.name, cs: pc.slug, d: p.date.slice(0, 10), st: st, img: p.bx_thumb || '', icon: th.icon, accent: th.accent };
155        });
156      });
157  }
158  function fetchPage(page, acc) {
159    return fetch(REST + '/posts?per_page=100&page=' + page + '&_fields=id,link,title,excerpt,date,categories,bx_thumb,bx_excerpt')
160      .then(function (r) { return r.ok ? r.json() : []; })
161      .then(function (b) {
162        acc = acc.concat(b);
163        return (b.length === 100) ? fetchPage(page + 1, acc) : acc;
164      });
165  }
166
167  /* ───────── map ───────── */
168  function buildMap() {
169    POSTS.forEach(function (p) { p.st.forEach(function (ab) { STATE_COUNT[ab] = (STATE_COUNT[ab] || 0) + 1; }); });
170    var max = Math.max.apply(null, [1].concat(Object.keys(STATE_COUNT).map(function (k) { return STATE_COUNT[k]; })));
171    return fetch(GEO).then(function (r) { return r.json(); }).then(function (geo) {
172      var svg = '<svg viewBox="' + geo.vb + '" class="bx-usmap" role="group" aria-label="Compliance coverage — all 50 states" preserveAspectRatio="xMidYMid meet">';
173      var labels = '';
174      // Every state is covered: federal guidance applies nationwide. The heat
175      // only shows where we ALSO have state-specific deep-dives — no state is
176      // ever "empty", because clicking it surfaces the nationwide library too.
177      Object.keys(geo.states).forEach(function (ab) {
178        var s = geo.states[ab], n = STATE_COUNT[ab] || 0;
179        var lvl = n === 0 ? 0 : Math.min(4, Math.ceil(n / max * 4));
180        var al = AB2NAME[ab] + (n ? ', ' + n + ' state-specific guides plus nationwide' : ', nationwide coverage');
181        svg += '<path d="' + s.d + '" class="bx-st h' + lvl + ' on" data-ab="' + ab + '" tabindex="0" role="button" aria-label="' + al + '"></path>';
182        if (s.lx != null && n > 1) labels += '<text x="' + s.lx + '" y="' + s.ly + '" class="bx-stlabel">' + ab + '</text>';
183      });
184      // NE callout dots (state paths too small to click) — render all of them
185      Object.keys(geo.calls).forEach(function (ab) {
186        var n = STATE_COUNT[ab] || 0, c = geo.calls[ab], lvl = n === 0 ? 0 : Math.min(4, Math.ceil(n / max * 4));
187        var al = AB2NAME[ab] + (n ? ', ' + n + ' state-specific guides plus nationwide' : ', nationwide coverage');
188        svg += '<circle cx="' + c.cx + '" cy="' + c.cy + '" r="6" class="bx-st bx-stdot h' + lvl + ' on" data-ab="' + ab + '" tabindex="0" role="button" aria-label="' + al + '"></circle>';
189      });
190      svg += labels + '</svg>';
191      elMap.innerHTML = svg;
192      var tip = document.createElement('div'); tip.className = 'bx-maptip'; tip.hidden = true; elMap.appendChild(tip);
193      elMap.querySelectorAll('.bx-st.on').forEach(function (el) {
194        var ab = el.getAttribute('data-ab'), n = STATE_COUNT[ab] || 0;
195        el.addEventListener('click', function () { toggleState(ab); });
196        el.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleState(ab); } });
197        el.addEventListener('mousemove', function (e) {
198          var r = elMap.getBoundingClientRect();
199          tip.innerHTML = '<b>' + AB2NAME[ab] + '</b><span>' + (n ? n + ' state-specific + nationwide' : 'Nationwide coverage') + '</span>';
200          tip.style.left = (e.clientX - r.left) + 'px';
201          tip.style.top = (e.clientY - r.top) + 'px';
202          tip.hidden = false;
203        });
204        el.addEventListener('mouseleave', function () { tip.hidden = true; });
205      });
206    });
207  }
208  function toggleState(ab) { filter.state = (filter.state === ab) ? '' : ab; sync(); }
209
210  /* ───────── chips ───────── */
211  function buildChips() {
212    var counts = {};
213    POSTS.forEach(function (p) { if (p.cs) counts[p.cs] = (counts[p.cs] || { n: 0, name: p.c }); if (p.cs) counts[p.cs].n++; });
214    var skip = { uncategorized: 1, blog: 1 };
215    var top = Object.keys(counts).filter(function (s) { return !skip[s]; }).sort(function (a, b) { return counts[b].n - counts[a].n; }).slice(0, 9);
216    var html = '<button class="bx-chip is-active" data-cat="" aria-pressed="true">All topics</button>';
217    top.forEach(function (slug) { html += '<button class="bx-chip" data-cat="' + slug + '" aria-pressed="false">' + esc(counts[slug].name) + ' <span>' + counts[slug].n + '</span></button>'; });
218    elChips.innerHTML = html;
219    elChips.querySelectorAll('.bx-chip').forEach(function (ch) {
220      ch.addEventListener('click', function () { filter.cat = ch.dataset.cat; sync(); });
221    });
222  }
223
224  /* ───────── filter + render ───────── */
225  function matches(p) {
226    if (filter.cat && p.cs !== filter.cat) return false;
227    if (filter.state && p.st.indexOf(filter.state) === -1 && p.st.length !== 0) return false; // state-specific OR nationwide
228    if (filter.q) {
229      var q = filter.q.toLowerCase();
230      if ((p.t + ' ' + p.e + ' ' + p.c).toLowerCase().indexOf(q) === -1) return false;
231    }
232    return true;
233  }
234  function fmtDate(iso) { // "2026-06-11" -> "June 11, 2026", matching the server-rendered cards
235    var d = new Date(iso + 'T00:00:00');
236    return isNaN(d) ? iso : d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
237  }
238  function cardHTML(p, i) {
239    return '<a class="bx-card bx-reveal" style="--d:' + ((i % 6) * 45) + 'ms" href="' + p.u + '">' +
240      '<div class="bx-card-media">' + (p.img
241        ? '<img src="' + p.img + '" alt="" width="768" height="403" loading="lazy">'
242        : '<div class="bx-thumb" style="--accent:' + p.accent + '"><span class="bx-thumb-icon"><svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">' + ICONS[p.icon] + '</svg></span></div>') + '</div>' +
243      '<div class="bx-card-body"><span class="bx-eyebrow">' + esc(p.c) + '</span>' +
244        '<h3>' + esc(p.t) + '</h3><p class="bx-card-excerpt">' + esc(p.e) + '</p>' +
245        '<div class="bx-card-meta"><span class="bx-dot"></span>' + fmtDate(p.d) + '</div></div></a>';
246  }
247  function render(reset) {
248    var list = POSTS.filter(matches);
249    // Unfiltered view mirrors the server grid: the Compliance Wire stream stays
250    // out until the reader searches, picks a state, or clicks its chip.
251    var isFiltered = !!(filter.q || filter.cat || filter.state);
252    if (!isFiltered && HIDE) list = list.filter(function (p) { return p.cs !== HIDE; });
253    if (filter.state) {  // surface the state-specific guides first, nationwide below
254      list = list.slice().sort(function (a, b) {
255        return (a.st.indexOf(filter.state) !== -1 ? 0 : 1) - (b.st.indexOf(filter.state) !== -1 ? 0 : 1);
256      });
257    }
258    // Default view: keep the featured lead story out of the grid (it is already
259    // showcased above — mirrors the server grid's post__not_in). Under an active
260    // filter the grid IS the result set, so the featured post participates and
261    // the count always equals what is actually listed.
262    var gridList = (!isFiltered && FEATURED) ? list.filter(function (p) { return p.id !== FEATURED; }) : list;
263    if (reset) shown = PAGE;
264    var slice = gridList.slice(0, shown);
265    elResults.innerHTML = slice.map(cardHTML).join('') || '<p class="bx-empty">No articles match those filters. Clear one to widen the search.</p>';
266    if (filter.state) {
267      var spec = list.filter(function (p) { return p.st.indexOf(filter.state) !== -1; }).length;
268      elCount.textContent = list.length + ' guides relevant to ' + AB2NAME[filter.state] + (spec ? ' · ' + spec + ' ' + AB2NAME[filter.state] + '-specific' : ' (nationwide)');
269    } else {
270      elCount.textContent = list.length + (list.length === 1 ? ' article' : ' articles');
271    }
272    elMore.hidden = gridList.length <= shown;
273    // active filter pills
274    var pills = '';
275    if (filter.state) pills += pill('state', AB2NAME[filter.state]);
276    if (filter.cat) { var nm = (POSTS.find(function (p) { return p.cs === filter.cat; }) || {}).c || filter.cat; pills += pill('cat', nm); }
277    if (filter.q) pills += pill('q', '“' + esc(filter.q) + '”');
278    if ((filter.state ? 1 : 0) + (filter.cat ? 1 : 0) + (filter.q ? 1 : 0) >= 2) pills += '<button class="bx-clearall" data-clearall="1">Clear all</button>';
279    elActive.innerHTML = pills;
280    elActive.querySelectorAll('[data-clear]').forEach(function (b) { b.addEventListener('click', function () { filter[b.dataset.clear] = ''; if (b.dataset.clear === 'q') elSearch.value = ''; sync(); }); });
281    var clearAll = elActive.querySelector('[data-clearall]');
282    if (clearAll) clearAll.addEventListener('click', function () { filter.state = ''; filter.cat = ''; filter.q = ''; elSearch.value = ''; sync(); });
283    // reflect chip + map active state
284    elChips.querySelectorAll('.bx-chip').forEach(function (c) { var on = c.dataset.cat === filter.cat; c.classList.toggle('is-active', on); c.setAttribute('aria-pressed', on); });
285    elMap.querySelectorAll('.bx-st').forEach(function (s) { s.classList.toggle('sel', s.getAttribute('data-ab') === filter.state); });
286  }
287  function pill(kind, label) { return '<button class="bx-pill" data-clear="' + kind + '">' + label + ' <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M6 6l12 12M18 6L6 18"/></svg></button>'; }
288  function sync() { render(true); }
289
290  /* ───────── boot ───────── */
291  loadPosts().then(function () {
292    buildChips();
293    render(true);
294    // The JS grid pages with "Show more", so the server pagination becomes a
295    // second, competing pager the moment we boot. Hide it VISUALLY only: the
296    // links must stay in the served HTML because they are the crawl path to
297    // the whole library for anything that does not execute JS (see
298    // template-blog.blade.php). display:none via attribute, not removal.
299    var pag = document.querySelector('.bx-pagination');
300    if (pag) pag.setAttribute('data-enhanced', '');
301    return buildMap();
302  }).then(function () {
303    render(false);
304  }).catch(function (e) { /* leave server-rendered fallback in place */ });
305
306  var t;
307  if (elSearch) elSearch.addEventListener('input', function () { clearTimeout(t); t = setTimeout(function () { filter.q = elSearch.value.trim(); sync(); }, 140); });
308  if (elMore) elMore.addEventListener('click', function () { shown += PAGE; render(false); });
309})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.