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.