PageSourceSearch

https://goodsmartidea.com/gsi-search.js?v=dev

js goodsmartidea.com collected 2026-10-09 23:13:42 UTC 29,221 bytes, 602 lines download raw bytes

1/* ─── GSI Search ─── v1.0 ───
2 * Two search features:
3 * 1) Site-wide command palette (⌘K / Ctrl+K) — searches pages + blog posts
4 * 2) Learn Hub inline search — articles + videos only
5 * Injected into static Next.js export. No framework dependencies.
6 */
7(function () {
8  "use strict";
9
10  // ── SVG Icons ──
11  const ICONS = {
12    search: '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>',
13    page: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14,2 14,8 20,8"/></svg>',
14    article: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8m8-4h-8m5 8h-5"/></svg>',
15    video: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><polygon points="5,3 19,12 5,21"/></svg>',
16    arrow: '<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>',
17    x: '<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12"/></svg>',
18  };
19
20  // ── Public Pages Index ──
21  const SITE_PAGES = [
22    { title: "Home", path: "/", desc: "AI Agency in Cleveland, OH" },
23    { title: "About Us", path: "/about/", desc: "Meet the GSI team and our story" },
24    { title: "Case Studies", path: "/case-studies/", desc: "Real results from real clients" },
25    { title: "Contact / Book a Call", path: "/contact/", desc: "Get in touch with our team" },
26    { title: "Learn Hub", path: "/learn-hub/", desc: "AI tutorials, guides, and videos" },
27    { title: "Our Process", path: "/process/", desc: "How we work with clients" },
28    { title: "Results", path: "/results/", desc: "Proven outcomes and metrics" },
29    // Solutions
30    { title: "All Solutions", path: "/solutions/", desc: "Build, Grow, and Run your business with AI" },
31    { title: "Build Your Product", path: "/solutions/build/", desc: "Custom AI and rapid MVP development" },
32    { title: "Custom AI Solutions", path: "/solutions/build/custom-ai/", desc: "Tailored AI systems for your business" },
33    { title: "Rapid MVP Builder", path: "/solutions/build/rapid-mvp/", desc: "Launch your product in weeks, not months" },
34    { title: "Grow Your Business", path: "/solutions/grow/", desc: "Content, social media, and website growth" },
35    { title: "AI Content Engine", path: "/solutions/grow/content-engine/", desc: "Automated content production at scale" },
36    { title: "Self-Growing Websites", path: "/solutions/grow/self-growing-websites/", desc: "Websites that create their own content" },
37    { title: "AI Social Media Manager", path: "/solutions/grow/social-media-manager/", desc: "Automated social media management" },
38    { title: "Run Your Business", path: "/solutions/run/", desc: "Customer support and operations automation" },
39    { title: "AI Customer Support", path: "/solutions/run/customer-support/", desc: "Resolve 80% of tickets without hiring" },
40    { title: "AI Operations Autopilot", path: "/solutions/run/operations-autopilot/", desc: "Automate back-office operations" },
41    { title: "AI Outbound Sales", path: "/solutions/run/outbound-sales/", desc: "AI-powered lead generation and outreach" },
42    // Legal
43    { title: "Privacy Policy", path: "/privacy/", desc: "How we handle your data" },
44    { title: "Terms of Service", path: "/terms/", desc: "Terms and conditions" },
45    { title: "Cookie Policy", path: "/cookies/", desc: "Our cookie usage policy" },
46  ];
47
48  // ── Video data (from Learn Hub) ──
49  const VIDEOS = [
50    { title: "BEST Way to Self-Host n8n FREE in Cloud Forever", ytId: "PnX8mC80zqE", duration: "25:03", tag: "Tutorial" },
51    { title: "180 Days of Using n8n in 10 Minutes", ytId: "N5grx49clIE", duration: "10:00", tag: "Deep Dive" },
52    { title: "n8n YouTube Transcript Scraper with 40,000 APIs", ytId: "ugck4dZc7HQ", duration: "15:47", tag: "Tutorial" },
53    { title: "Switched from Zapier to n8n — This Happened", ytId: "pHhNBPUtpe8", duration: "12:38", tag: "Case Study" },
54    { title: "Scrape UNLIMITED Leads with Simple n8n Automation", ytId: "B0owzDMGSpQ", duration: "18:32", tag: "Tutorial" },
55    { title: "How I Got Google AI Pro Free Until July 2026", ytId: "ABOizM5FSkg", duration: "5:00", tag: "AI Tools" },
56  ];
57
58  // ── Blog articles (built from static pages at deploy time) ──
59  const ARTICLES = [
60    { title: "5 AI Automations That Paid for Themselves in 30 Days", slug: "5-ai-automations-that-paid-for-themselves-in-30-days" },
61    { title: "Why Cleveland Businesses Are Switching to AI Agencies (And What It Costs)", slug: "ai-automation-agency-cleveland-2026" },
62    { title: "AI Automation for Small Business: 7 Workflows That Pay for Themselves in 30 Days", slug: "ai-automation-small-business-workflows-2026" },
63    { title: "5 AI Automation Workflows That Actually Save Small Businesses Money in 2026", slug: "ai-automation-workflows-small-business-2026-2" },
64    { title: "AI Cold Email vs Human BDRs: The 2026 Sales Showdown", slug: "ai-cold-email-vs-human-bdrs-2026" },
65    { title: "Building an AI Content Engine: From Zero to 50 Articles a Month", slug: "ai-content-engine-for-business-guide-2026" },
66    { title: "AI Customer Support Automation: How to Handle 80% of Tickets Without Hiring", slug: "ai-customer-support-automation-guide-2026" },
67    { title: "AI Customer Support: Beyond Chatbots to Actual Resolution", slug: "ai-customer-support-beyond-chatbots-2026" },
68    { title: "AI Email Marketing for Service Businesses: A Field Guide", slug: "ai-email-marketing-for-service-businesses-a-field-guide" },
69    { title: "AI for Restaurants: One System, Three Problems Solved", slug: "ai-for-restaurants-one-system-three-problems-solved" },
70    { title: "How to Make AI-Generated Content That Doesn\u2019t Sound Like a Robot Wrote It", slug: "ai-generated-content-sound-human-2026-2" },
71    { title: "Building an MVP in 2026: Why AI-Accelerated Development Wins", slug: "ai-mvp-development-2026" },
72    { title: "The Small Business Owner\u2019s Guide to AI Operations Automation", slug: "ai-operations-automation-small-business-guide-2026" },
73    { title: "AI Outbound Sales Tools: The 2026 Stack That Replaces Your BDR Team", slug: "ai-outboun
73d-sales-tools-2026-guide" },
74    { title: "How AI Is Replacing the $10K/Month Marketing Agency in 2026", slug: "ai-replacing-marketing-agency-2026" },
75    { title: "The Real ROI of AI Social Media Management", slug: "ai-social-media-management-roi-2026" },
76    { title: "40 BIG Mistakes Brands and Companies Make On Social Media", slug: "big-mistakes-brands-and-companies-make-on-social-media" },
77    { title: "Build an AI Content Pipeline This Weekend (No Agency Needed)", slug: "build-an-ai-content-pipeline-this-weekend-no-agency-needed" },
78    { title: "Build n8n AI Agents: AI Workflow Automations", slug: "build-n8n-ai-agents" },
79    { title: "How a Cleveland E-Commerce Brand Cut Ad Spend 60% with AI-Powered Campaign Management", slug: "case-study-ecommerce-ai-campaign-management" },
80    { title: "How an Insurance Agency Cut Back-Office Time by 70% with AI Automation", slug: "case-study-insurance-agency-back-office-automation" },
81    { title: "How a SaaS Startup Went from Idea to MVP in 6 Weeks Using AI-Accelerated Development", slug: "case-study-saas-mvp-ai-development" },
82    { title: "Claude + ChatGPT Together: The Dual-AI Workflow That Works", slug: "claude-chatgpt-together-the-dual-ai-workflow-that-works" },
83    { title: "Cleveland AI Automation: Why Local Businesses Are Making the Switch in 2026", slug: "cleveland-ai-automation-local-business-2026" },
84    { title: "Content Chaos to Content Engine: Managing Multi-Platform Publishing in 2026", slug: "content-chaos-to-content-engine-multi-platform-2026" },
85    { title: "How to Self-Host n8n for FREE in 10 Minutes", slug: "how-to-self-host-n8n-for-free-in-10-minutes-step-by-step-2025-guide" },
86    { title: "How to Use SHA-256 Hashing in Google Sheets for Secure Conversion Uploads to Google Ads", slug: "how-to-use-sha-256-hashing-in-google-sheets-for-secure-conversion-uploads-to-google-ads" },
87    { title: "How We Built a Lead Qualification Bot That Books Meetings", slug: "how-we-built-a-lead-qualification-bot-that-books-meetings" },
88    { title: "How We Generated 6,000 B2B Leads in 60 Minutes", slug: "how-we-generated-6000-b2b-leads-in-60-minutes-the-ultimate-no-code-system-using-n8n-google-maps-and-searchapi" },
89    { title: "LLM Agents: Revolutionizing Complex Task Automation with AI Systems", slug: "llm-agents-revolutionizing-complex-task-automation-with-ai-systems" },
90    { title: "Maximize Your PPC Budget: Strategies to Minimize Wasted Ad Spend", slug: "maximize-your-ppc-budget-strategies-to-minimize-wasted-ad-spend" },
91    { title: "MCP Servers Explained: The Complete Guide for 2026", slug: "mcp-servers-explained-guide-2026-2" },
92    { title: "n8n vs Zapier vs Make: Real Cost Breakdown for Small Biz", slug: "n8n-vs-zapier-vs-make-real-cost-breakdown-for-small-biz" },
93    { title: "The Self-Growing Website: An AI Guide for 2026", slug: "self-growing-website-ai-guide-2026" },
94    { title: "Social Media Marketing Strategy for Small Business Owners", slug: "social-media-marketing-strategy-for-small-business-owners" },
95    { title: "Automating YouTube Video Transcription with RapidAPI & n8n", slug: "the-ultimate-guide-automating-youtube-video-transcription-with-rapidapi-n8n-no-code-needed" },
96    { title: "Trigger.dev AI Agents: The Complete Automation Guide for 2026", slug: "trigger-dev-ai-agents-automation-2026-2" },
97    { title: "Ways to Improve Social Media Customer Service", slug: "ways-to-improve-social-media-customer-service" },
98    // Blog posts (older /blog/ path)
99    { title: "10 Business Processes You Should Automate with AI Right Now", slug: "10-business-processes-to-automate-with-ai", blogPath: true },
100    { title: "AI Agency vs Traditional Agency: The Real Cost Comparison", slug: "ai-agency-vs-traditional-agency", blogPath: true },
101    { title: "AI Content That Doesn\u2019t Sound Like AI: Our Humanization Process", slug: "ai-content-that-doesnt-sound-like-ai", blogPath: true },
102    { title: "n8n vs Make vs Zapier: Which Automation Platform Is Right for Your Business?", slug: "n8n-vs-make-vs-zapier", blogPath: true },
103  ];
104
105  // ─────────────────────────────────────────────
106  // 1. SITE-WIDE SEARCH (Command Palette)
107  // ─────────────────────────────────────────────
108
109  function buildOverlay() {
110    const overlay = document.createElement("div");
111    overlay.className = "gsi-search-overlay";
112    overlay.id = "gsi-search-overlay";
113    overlay.innerHTML = `
114      <div class="gsi-search-modal" role="dialog" aria-label="Search">
115        <div class="gsi-search-input-wrap">
116          ${ICONS.search}
117          <input class="gsi-search-input" id="gsi-search-input" type="text"
118                 aria-label="Search pages, articles, videos"
119                 placeholder="Search pages, articles, videos..." autocomplete="off" />
120          <span class="gsi-search-kbd"><kbd>\u2318</kbd><kbd>K</kbd></span>
121        </div>
122        <div class="gsi-search-results" id="gsi-search-results"></div>
123        <div class="gsi-search-footer">
124          <div class="gsi-search-footer-keys">
125            <span><kbd>\u2191</kbd><kbd>\u2193</kbd> navigate</span>
126            <span><kbd>\u21B5</kbd> open</span>
127            <span><kbd>esc</kbd> close</span>
128          </div>
129          <span style="color:#E8C547;opacity:0.4">GSI</span>
130        </div>
131      </div>
132    `;
133    document.body.appendChild(overlay);
134
135    // Fix kbd display for Windows/Linux
136    if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) {
137      overlay.querySelectorAll(".gsi-search-kbd kbd").forEach(function (k, i) {
138        if (i === 0) k.textContent = "Ctrl";
139      });
140    }
141
142    return overlay;
143  }
144
145  function fuzzyMatch(query, text) {
146    var q = query.toLowerCase();
147    var t = text.toLowerCase();
148    if (t.includes(q)) return 2; // exact substring
149    // word-start matching
150    var words = q.split(/\s+/);
151    var allMatch = words.every(function (w) { return t.includes(w); });
152    if (allMatch) return 1;
153    return 0;
154  }
155
156  function searchAll(query) {
157    if (!query || query.length < 2) return { pages: [], articles: [], videos: [] };
158    var pages = [], articles = [], videos = [];
159
160    SITE_PAGES.forEach(function (p) {
161      var score = Math.max(fuzzyMatch(query, p.title), fuzzyMatch(query, p.desc || ""), fuzzyMatch(query, p.path));
162      if (score > 0) pages.push(Object.assign({ score: score }, p));
163    });
164
165    ARTICLES.forEach(function (a) {
166      var score = fuzzyMatch(query, a.title);
167      if (score > 0) articles.push(Object.assign({ score: score }, a));
168    });
169
170    VIDEOS.forEach(function (v) {
171      var score = Math.max(fuzzyMatch(query, v.title), fuzzyMatch(query, v.tag || ""));
172      if (score > 0) videos.push(Object.assign({ score: score }, v));
173    });
174
175    var sortFn = function (a, b) { return b.score - a.score; };
176    pages.sort(sortFn);
177    articles.sort(sortFn);
178    videos.sort(sortFn);
179
180    return { pages: pages.slice(0, 5), articles: articles.slice(0, 8), videos: videos.slice(0, 4) };
181  }
182
183  function renderResults(results, container) {
184    var html = "";
185    var idx = 0;
186
187    if (!results.pages.length && !results.articles.length && !results.videos.length) {
188      container.innerHTML = '<div class="gsi-search-empty"><strong>No results found</strong>Try a different search term</div>';
189      return;
190    }
191
192    if (results.pages.length) {
193      html += '<div class="gsi-search-group-label">Pages</div>';
194      results.pages.forEach(function (p) {
195        html += '<a class="gsi-search-item" data-idx="' + idx + '" href="' + p.path + '">' +
196          '<div class="gsi-search-item-icon">' + ICONS.page + '</div>' +
197          '<div class="gsi-search-item-text">' +
198            '<div class="gsi-search-item-title">' + escHtml(p.title) + '</div>' +
199            '<div class="gsi-search-item-desc">' + escHtml(p.desc || p.path) + '</div>' +
200          '</div>' +
201          '<div class="gsi-search-item-badge">' + ICONS.arrow + '</div>' +
202        '</a>';
203        idx++;
204      });
205    }
206
207    if (results.articles.length) {
208      html += '<div class="gsi-search-group-label">Articles</div>';
209      results.articles.forEach(function (a) {
210        var href = "/learn-hub/" + a.slug + "/";
211        html += '<a class="gsi-search-item" data-idx="' + idx + '" href="' + href + '">' +
212          '<div class="gsi-search-item-icon article">' + ICONS.article + '</div>' +
213          '<div class="gsi-search-item-text">' +
214            '<div class="gsi-search-item-title">' + escHtml(a.title) + '</div>' +
215            '<div class="gsi-search-item-desc">Learn Hub</div>' +
216          '</div>' +
217          '<div class="gsi-search-item-badge">' + ICONS.arrow + '</div>' +
218        '</a>';
219        idx++;
220      });
221    }
222
223    if (results.videos.length) {
224      html += '<div class="gsi-search-group-label">Videos</div>';
225      results.videos.forEach(function (v) {
226        html += '<a class="gsi-search-item" data-idx="' + idx + '" href="https://youtube.com/watch?v=' + v.ytId + '" target="_blank" rel="noopener">' +
227          '<div class="gsi-search-item-icon video">' + ICONS.video + '</div>' +
228          '<div class="gsi-search-item-text">' +
229            '<div class="gsi-search-item-title">' + escHtml(v.title) + '</div>' +
230            '<div class="gsi-search-item-desc">' + v.duration + ' \u00B7 ' + v.tag + '</div>' +
231          '</div>' +
232          '<div class="gsi-search-item-badge">' + ICONS.arrow + '</div>' +
233        '</a>';
234        idx++;
235      });
236    }
237
238    container.innerHTML = html;
239  }
240
241  function renderQuickLinks(container) {
242    var quick = [
243      { title: "Book a Call", path: "/contact/", icon: "page" },
244      { title: "Learn Hub", path: "/learn-hub/", icon: "article" },
245      { title: "All Solutions", path: "/solutions/", icon: "page" },
246      { title: "Case Studies", path: "/case-studies/", icon: "page" },
247    ];
248    var html = '<div class="gsi-search-group-label">Quick Links</div>';
249    quick.forEach(function (q, i) {
250      html += '<a class="gsi-search-item" data-idx="' + i + '" href="' + q.path + '">' +
251        '<div class="gsi-search-item-icon">' + ICONS[q.icon] + '</div>' +
252        '<div class="gsi-search-item-text">' +
253          '<div class="gsi-search-item-title">' + escHtml(q.title) + '</div>' +
254        '</div>' +
255        '<div class="gsi-search-item-badge">' + ICONS.arrow + '</div>' +
256      '</a>';
257    });
258    container.innerHTML = html;
259  }
260
261  function escHtml(s) {
262    var d = document.createElement("div");
263    d.textContent = s;
264    return d.innerHTML;
265  }
266
267  function initSiteSearch() {
268    var overlay = buildOverlay();
269    var input = document.getElementById("gsi-search-input");
270    var resultsEl = document.getElementById("gsi-search-results");
271    var activeIdx = -1;
272
273    function open() {
274      overlay.classList.add("open");
275      input.value = "";
276      input.focus();
277      renderQuickLinks(resultsEl);
278      activeIdx = -1;
279      document.body.style.overflow = "hidden";
280    }
281
282    function close() {
283      overlay.classList.remove("open");
284      document.body.style.overflow = "";
285    }
286
287    function setActive(idx) {
288      var items = resultsEl.querySelectorAll(".gsi-search-item");
289      items.forEach(function (el) { el.classList.remove("active"); });
290      if (idx >= 0 && idx < items.length) {
291        items[idx].classList.add("active");
292        items[idx].scrollIntoView({ block: "nearest" });
293      }
294      activeIdx = idx;
295    }
296
297    // ⌘K / Ctrl+K
298    document.addEventListener("keydown", function (e) {
299      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
300        e.preventDefault();
301        if (overlay.classList.contains("open")) close();
302        else open();
303      }
304      if (e.key === "Escape" && overlay.classList.contains("open")) {
305        e.preventDefault();
306        close();
307      }
308    });
309
310    // Click backdrop to close
311    overlay.addEventListener("click", function (e) {
312      if (e.target === overlay) close();
313    });
314
315    // Search input
316    input.addEventListener("input", function () {
317      var q = input.value.trim();
318      if (q.length < 2) {
319        renderQuickLinks(resultsEl);
320        activeIdx = -1;
321        return;
322      }
323      var results = searchAll(q);
324      renderResults(results, resultsEl);
325      activeIdx = -1;
326    });
327
328    // Keyboard nav
329    input.addEventListener("keydown", function (e) {
330      var items = resultsEl.querySelectorAll(".gsi-search-item");
331      var count = items.length;
332      if (e.key === "ArrowDown") {
333        e.preventDefault();
334        setActive(activeIdx < count - 1 ? activeIdx + 1 : 0);
335      } else if (e.key === "ArrowUp") {
336        e.preventDefault();
337        setActive(activeIdx > 0 ? activeIdx - 1 : count - 1);
338      } else if (e.key === "Enter") {
339        e.preventDefault();
340        if (activeIdx >= 0 && items[activeIdx]) {
341          items[activeIdx].click();
342        }
343      }
344    });
345
346    // Inject trigger button into nav
347    injectNavTrigger(open);
348  }
349
350  function injectNavTrigger(openFn) {
351    // Desktop: insert search button before CTA "Book a Call"
352    var nav = document.querySelector("nav");
353    if (!nav) return;
354
355    // Desktop CTA container (hidden on mobile, flex on md+)
356    var ctaDiv = nav.querySelector(".hidden.items-center.gap-3.md\\:flex");
357    if (ctaDiv) {
358      var btn = document.createElement("button");
359      btn.className = "gsi-search-trigger";
360      btn.setAttribute("aria-label", "Search (Cmd+K)");
361      btn.innerHTML = ICONS.search;
362      btn.addEventListener("click", function (e) {
363        e.preventDefault();
364        e.stopPropagation();
365        openFn();
366      });
367      ctaDiv.insertBefore(btn, ctaDiv.firstChild);
368    }
369
370    // Mobile: insert search button before hamburger
371    var mobileDiv = nav.querySelector(".relative.z-50.flex.items-center.gap-2.md\\:hidden");
372    if (mobileDiv) {
373      var mBtn = document.createElement("button");
374      mBtn.className = "gsi-search-trigger";
375      mBtn.setAttribute("aria-label", "Search");
376      mBtn.innerHTML = ICONS.search;
377      mBtn.addEventListener("click", function (e) {
378        e.preventDefault();
379        e.stopPropagation();
380        openFn();
381      });
382      mobileDiv.insertBefore(mBtn, mobileDiv.firstChild);
383    }
384  }
385
386  // ─────────────────────────────────────────────
387  // 2. LEARN HUB INLINE SEARCH
388  // ─────────────────────────────────────────────
389
390  function initLearnHubSearch() {
391    // Only on /learn-hub/ or /youtube/ pages
392    if (!/^\/(learn-hub|youtube)(\/|\/index\.html)?$/.test(window.location.pathname)) return;
393
394    // Find the hero section — look for the subtitle text
395    var sections = document.querySelectorAll("section");
396    var heroSection = null;
397    for (var i = 0; i < sections.length; i++) {
398      if (sections[i].querySelector("h1") || sections[i].textContent.includes("Learn Hub")) {
399        heroSection = sections[i];
400        break;
401      }
402    }
403    if (!heroSection) return;
404
405    // Find the sticky filter bar (the scrollable pill buttons)
406    var filterBar = document.querySelector('[class*="sticky"][class*="top-"]') ||
407                    heroSection.parentElement.querySelector('[class*="overflow-x-auto"]');
408
409    // Create search bar container
410    var wrap = document.createElement("div");
411    wrap.className = "gsi-lh-search-wrap";
412    wrap.style.marginBottom = "24px";
413    wrap.style.marginTop = "8px";
414    wrap.innerHTML =
415      '<div class="gsi-lh-search-icon">' + ICONS.search + '</div>' +
416      '<input class="gsi-lh-search" type="text" placeholder="Search articles and videos..." autocomplete="off" />' +
417      '<button class="gsi-lh-search-clear">' + ICONS.x + '</button>' +
418      '<div class="gsi-lh-results" id="gsi-lh-results"></div>';
419
420    // Insert after the filter bar if found, otherwise after first section content
421    if (filterBar) {
422      filterBar.parentNode.insertBefore(wrap, filterBar.nextSibling);
423    } else {
424      // Insert into hero section, after the subtitle area
425      var heroInner = heroSection.querySelector('[class*="max-w"]') || heroSection;
426      // Look for the description paragraph
427      var descP = heroSection.querySelector("p");
428      if (descP && descP.parentElement) {
429        descP.parentElement.insertBefore(wrap, descP.nextSibling);
430      } else {
431        heroInner.appendChild(wrap);
432      }
433    }
434
435    var searchInput = wrap.querySelector(".gsi-lh-search");
436    var clearBtn = wrap.querySelector(".gsi-lh-search-clear");
437    var resultsDiv = wrap.querySelector(".gsi-lh-results");
438
439    function searchLearnHub(q) {
440      if (!q || q.length < 2) return [];
441      var results = [];
442
443      ARTICLES.forEach(function (a) {
444        if (a.blogPath) return; // Skip /blog/ posts in LH search — only learn-hub articles
445        var score = fuzzyMatch(q, a.title);
446        if (score > 0) results.push({ type: "article", title: a.title, slug: a.slug, score: score });
447      });
448
449      VIDEOS.forEach(function (v) {
450        var score = Math.max(fuzzyMatch(q, v.title), fuzzyMatch(q, v.tag || ""));
451        if (score > 0) results.push({ type: "video", title: v.title, ytId: v.ytId, duration: v.duration, tag: v.tag, score: score });
452      });
453
454      results.sort(function (a, b) { return b.score - a.score; });
455      return results.slice(0, 10);
456    }
457
458    function renderLHResults(results) {
459      if (!results.length) {
460        resultsDiv.innerHTML = '<div style="padding:16px;text-align:center;color:#6B7B6B;font-size:13px;">No articles or videos found</div>';
461        resultsDiv.classList.add("open");
462        return;
463      }
464      var html = "";
465      results.forEach(function (r) {
466        if (r.type === "article") {
467          html += '<a class="gsi-lh-result-item" href="/learn-hub/' + r.slug + '/">' +
468            '<div class="gsi-lh-result-icon article">' + ICONS.article + '</div>' +
469            '<div class="gsi-lh-result-title">' + escHtml(r.title) + '</div>' +
470            '<div class="gsi-lh-result-type">Article</div>' +
471          '</a>';
472        } else {
473          html += '<a class="gsi-lh-result-item" href="https://youtube.com/watch?v=' + r.ytId + '" target="_blank" rel="noopener">' +
474            '<div class="gsi-lh-result-icon video">' + ICONS.video + '</div>' +
475            '<div class="gsi-lh-result-title">' + escHtml(r.title) + '</div>' +
476            '<div class="gsi-lh-result-type">Video</div>' +
477          '</a>';
478        }
479      });
480      resultsDiv.innerHTML = html;
481      resultsDiv.classList.add("open");
482    }
483
484    searchInput.addEventListener("input", function () {
485      var q = searchInput.value.trim();
486      clearBtn.classList.toggle("visible", q.length > 0);
487      if (q.length < 2) {
488        resultsDiv.classList.remove("open");
489        return;
490      }
491      renderLHResults(searchLearnHub(q));
492    });
493
494    searchInput.addEventListener("focus", function () {
495      if (searchInput.value.trim().length >= 2) {
496        renderLHResults(searchLearnHub(searchInput.value.trim()));
497      }
498    });
499
500    clearBtn.addEventListener("click", function () {
501      searchInput.value = "";
502      clearBtn.classList.remove("visible");
503      resultsDiv.classList.remove("open");
504      searchInput.focus();
505    });
506
507    // Close dropdown on outside click
508    document.addEventListener("click", function (e) {
509      if (!wrap.contains(e.target)) {
510        resultsDiv.classList.remove("open");
511      }
512    });
513
514    // Escape key closes dropdown
515    searchInput.addEventListener("keydown", function (e) {
516      if (e.key === "Escape") {
517        resultsDiv.classList.remove("open");
518        searchInput.blur();
519      }
520    });
521  }
522
523  // ── Init with retry for React hydration ──
524  function init() {
525    initSiteSearch();
526    initLearnHubSearch();
527  }
528
529  function waitForNav(cb, retries) {
530    retries = retries || 0;
531    var nav = document.querySelector("nav");
532    var ctaDiv = nav && nav.querySelector(".hidden.items-center.gap-3.md\\:flex");
533    // Check if our trigger already exists
534    if (ctaDiv && ctaDiv.querySelector(".gsi-search-trigger")) return; // already injected
535    if (nav && ctaDiv && retries > 2) {
536      cb();
537      return;
538    }
539    if (retries < 30) {
540      setTimeout(function () { waitForNav(cb, retries + 1); }, 200);
541    }
542  }
543
544  // Build overlay immediately (it's appended to body, not inside React tree)
545  function initOverlayOnly() {
546    var overlay = document.getElementById("gsi-search-overlay");
547    if (overlay) return; // already built
548    initSiteSearch();
549  }
550
551  // Re-inject nav triggers after React hydration
552  function reinjectNavTriggers() {
553    var nav = document.querySelector("nav");
554    if (!nav) return;
555    // Remove any existing triggers first
556    nav.querySelectorAll(".gsi-search-trigger").forEach(function (el) { el.remove(); });
557
558    var openFn = function () {
559      var overlay = document.getElementById("gsi-search-overlay");
560      if (overlay) {
561        overlay.classList.add("open");
562        var input = document.getElementById("gsi-search-input");
563        if (input) { input.value = ""; input.focus(); }
564        var resultsEl = document.getElementById("gsi-search-results");
565        if (resultsEl) renderQuickLinks(resultsEl);
566        document.body.style.overflow = "hidden";
567      }
568    };
569    injectNavTrigger(openFn);
570  }
571
572  // Watch for React hydration replacing nav children
573  function observeNav() {
574    var nav = document.querySelector("nav");
575    if (!nav) return;
576    var observer = new MutationObserver(function () {
577      // If our trigger was removed, re-inject
578      if (!nav.querySelector(".gsi-search-trigger")) {
579        reinjectNavTriggers();
580      }
581    });
582    observer.observe(nav, { childList: true, subtree: true });
583  }
584
585  function start() {
586    // Build the overlay + keyboard shortcuts immediately
587    initOverlayOnly();
588    initLearnHubSearch();
589
590    // Wait for React to finish hydrating, then inject nav triggers
591    waitForNav(function () {
592      reinjectNavTriggers();
593      observeNav();
594    });
595  }
596
597  if (document.readyState === "loading") {
598    document.addEventListener("DOMContentLoaded", start);
599  } else {
600    start();
601  }
602})();

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.