Creating knowledgebases with search fields in the site builder
The code below is incredibly powerful in that it allows you to create Knowledgebases inside our website builder with search functionality. It can also easily be added to membership areas to allow members to access specific lessons more easily.
Please note, the code is designed to work with published sites, not preview links. You MUST publish the site for it to work properly as the url's it directs you to are /link, whereas preview links require /page/preview/{site_id}/link
This code is in 2 parts, and needs to go into 2 areas. If you want the search box on every page of your website (like a knowledgebase or course) then you want to add this to "Site Settings", however if you only want this on 1 page or several pages of your site, then add it to each page in "Page Settings".
This first code snippet needs to go in the Header/Head.
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet"> <!-- Include Font Awesome for the search icon --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <style> /* --- Open Search Button --- */ #open-search-button { background-color: transparent; border: 2px solid #848484; /* Updated border */ color: #848484; /* Updated text color */ width: 250px; height: 50px; font-size: 16px; cursor: pointer; position: absolute; top: 20px; right: 20px; z-index: 1000; border-radius: 8px; transition: box-shadow 0.3s ease; text-align: left; padding-left: 15px; font-family: Poppins; } /* Search icon styling within the button */ #open-search-button i { margin-right: 8px; /* Space between icon and text */ } #open-search-button:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } /* --- Search Popup --- */ #search-popup { display: none; position: fixed; top: 10%; /* Default top positioning */ left: 50%; transform: translateX(-50%); width: 60%; /*Default width*/ max-width: 800px; height: auto; max-height: 80vh; padding: 30px; background-color: white; border: 1px solid #ccc; border-radius: 16px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); z-index: 1001; overflow-y: auto; /* Enable vertical scrolling */ margin-bottom: 10%; } #search-popup .close-button { position: absolute; top: 0px; /*remove negative margins*/ right: 0px; /*remove negative margins*/ font-size: 32px; cursor: pointer; background: none; border: none; color: #aaa; transition: color 0.3s ease; } #search-popup .close-button:hover { color: black; } /* --- Overlay --- */ .overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1000; } /* --- Search Box and Results (Inside Popup) --- */ .search-container { margin-bottom: 20px; display: flex; justify-content: center !important; align-items: center; width: 100%; } #search-input { padding: 10px; padding-left: 35px; /* Make space for the icon */ width: 100%; border: 1px solid #ccc; border-radius: 4px; margin-right: 0px; font-size: 16px; height: 50px !important; /*border-top-right-radius: 0px !important; border-bottom-right-radius: 0px !important;*/ font-family: Poppins; /* Added: Font family */ background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi bi-search" viewBox="0 0 16 16"><path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/></svg>'); /* Search icon as background image */ background-repeat: no-repeat; /* Don't repeat the icon */ background-position: 10px center; /* Position the icon */ margin-right: 10px; } #search-button { padding: 10px 15px; background-color: #117aeb !important; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; width: 50% !important; height: 50px !important; margin: 0px !important; /*border-top-left-radius: 0px !important; border-bottom-left-radius: 0px !important;*/ font-family: Poppins; } #search-button:hover { background-color: #0056b3; } #search-results { border-top: 1px solid #eee; padding-top: 15px; font-family: Poppins; max-height: 60vh; /* Limit height, adjust as needed */ overflow-y: auto; } #search-results p { font-family: Poppins; } .search-result-item { margin-bottom: 0px; /* Removed bottom margin */ } .search-result-item h3 { margin-bottom: 0px; /* Removed bottom margin */ font-size: 18px; font-family: Poppins; font-weight: 400; } .search-result-item h3 a { color: black; text-decoration: none; padding: 10px; /* Increased padding */ display: inline-block; width: 100%; /* Make the link fill the container */ } .search-result-item h3 a:hover { background: #848484 !important; /* Updated hover background */ color: white !important; border-radius: 3px !important; text-decoration: none !important; } /* --- Media Query for Smaller Screens --- */ @media (max-width: 768px) { #search-popup { top: 0; width: 100%; max-width: none; transform: none; left: 0; border-radius: 0; } } </style>
This next code snippet needs to go in Body Start
<button id="open-search-button"><i class="fas fa-search"></i>Search Articles</button> <div id="search-popup"> <button class="close-button" aria-label="Close search popup">×</button> <div class="search-container"> <input type="text" id="search-input" placeholder="Search Articles"> <button id="search-button">Search</button> </div> <div id="search-results"></div> </div> <div class="overlay"></div> <script>document.addEventListener('DOMContentLoaded', async () => { // --- Configuration --- const siteID = 'SITE ID HERE - AFTER PAGE/PREVIEW/ IN A PREVIEW LINK'; const publicKey = 'CREATE A ROLE WITH ACCESS TO WEBSITE > VIEW, THEN CREATE ACCESS KEY AND PUT PUBLIC KEY HERE'; const privateKey = 'CREATE A ROLE WITH ACCESS TO WEBSITE > VIEW, THEN CREATE ACCESS KEY AND PUT PUBLIC KEY HERE'; const baseApiUrl = 'https://app.crmconnect.co'; const searchInput = document.getElementById('search-input'); const searchButton = document.getElementById('search-button'); const searchResults = document.getElementById('search-results'); const openSearchButton = document.getElementById('open-search-button'); const searchPopup = document.getElementById('search-popup'); const overlay = document.querySelector('.overlay'); const closeButton = searchPopup.querySelector('.close-button'); // --- Helper Functions --- function constructApiUrl(siteId) { const apiPath = '/api/design/site/'; return `${baseApiUrl}${apiPath}${siteId}?public_key=${publicKey}&private_key=${privateKey}`; } function constructPreviewUrl(siteId, slug) { return `${baseApiUrl}/page/preview/${siteId}/${slug}`; } function getRootDomain(url) { try { const parsedUrl = new URL(url); return parsedUrl.hostname; } catch (error) { console.error("Invalid URL:", url, error); return null; } } // --- 1. Fetch Page Titles (No Caching) --- async function fetchPageTitles() { const API_ENDPOINT = constructApiUrl(siteID); try { console.log("Fetching data from API (no cache):", API_ENDPOINT); const response = await fetch(API_ENDPOINT); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } const data = await response.json(); console.log("API Response Data:", data); if (!data || !data.data || !Array.isArray(data.data.pages)) { throw new Error('Invalid API response format.'); } const pages = data.data.pages.map(page => { // ALWAYS use relative URLs const pageUrl = page.slug ? `/${page.slug}` : '/'; return { title: page.page_name, url: pageUrl, }; }); if (!pages.every(page => typeof page.title === 'string' && typeof page.url === 'string')) { throw new Error("Extracted page data is invalid"); } return pages; } catch (error) { console.error('Error fetching page titles:', error); searchResults.innerHTML = `<p>Error loading page data. Please try again later.</p>`; return []; // Return an empty array on error } } // --- 2. Perform the Search (Improved Ordering) --- async function performSearch() { const searchTerm = searchInput.value.trim().toLowerCase(); searchResults.innerHTML = ''; // Clear results console.log("Search Term:", searchTerm); if (!searchTerm) { searchResults.innerHTML = ''; return; } let pageTitles = await fetchPageTitles(); // Fetch directly (no caching) console.log("Page Titles:", pageTitles); if(!pageTitles || pageTitles.length === 0){ //check for null or undefined searchResults.innerHTML = '<p>Error: No page data available.</p>'; return; } const searchWords = searchTerm.split(/\s+/); pageTitles = pageTitles.map(page => { let matchCount = 0; const pageTitleLower = page.title.toLowerCase(); for (const word of searchWords) { if (pageTitleLower.includes(word)) { matchCount++; } } return { ...page, matchCount }; }); pageTitles.sort((a, b) => b.matchCount - a.matchCount); let resultsFound = false; for (const page of pageTitles) { if (page.matchCount > 0) { const resultElement = document.createElement('div'); resultElement.classList.add('search-result-item'); resultElement.innerHTML = `<h3><a href="${page.url}">${page.title}</a></h3>`; searchResults.appendChild(resultElement); resultsFound = true; } } if (!resultsFound) { searchResults.innerHTML = '<p>No results found.</p>'; } } // --- Event Listeners --- // Open the popup openSearchButton.addEventListener('click', () => { searchPopup.style.display = 'block'; overlay.style.display = 'block'; searchInput.focus(); }); // Close the popup closeButton.addEventListener('click', () => { searchPopup.style.display = 'none'; overlay.style.display = 'none'; }); // Search on button click searchButton.addEventListener('click', performSearch); // Search on Enter key press within the input field searchInput.addEventListener('keypress', (event) => { if (event.key === 'Enter') { performSearch(); } });}); </script>