Complete Search Page: End-to-End Example
Complete Search Page: End-to-End Example
This is a complete, production-ready search page that you can copy, paste, and adapt. It includes a search box, results with highlighting, pagination, a language facet sidebar, autocomplete, and spellcheck suggestions — all in a single HTML file.
This example uses keyword search (eDisMax) which works directly from the browser with CORS enabled. For full hybrid search with AI/vector, see the PHP and Node.js examples in the Code Examples article.
The Complete HTML File
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Search</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #333; background: #f8f9fa; } .container { max-width: 1100px; margin: 0 auto; padding: 20px; } .header { text-align: center; padding: 40px 0 30px; } .header h1 { font-size: 28px; margin-bottom: 16px; } /* Search box */ .search-form { display: flex; max-width: 640px; margin: 0 auto 30px; } .search-form input { flex: 1; padding: 12px 16px; font-size: 16px; border: 2px solid #ddd; border-right: none; border-radius: 8px 0 0 8px; outline: none; } .search-form input:focus { border-color: #e8650a; } .search-form button { padding: 12px 28px; font-size: 16px; background: #e8650a; color: white; border: 2px solid #e8650a; border-radius: 0 8px 8px 0; cursor: pointer; font-weight: 600; } /* Autocomplete */ .ac-wrap { position: relative; flex: 1; } .ac-list { position: absolute; top: 100%; left: 0; right: 0; z-index: 100; background: white; border: 1px solid #ddd; border-top: none; border-radius: 0 0 8px 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); display: none; max-height: 300px; overflow-y: auto; } .ac-list a { display: block; padding: 10px 16px; text-decoration: none; color: #333; border-bottom: 1px solid #f0f0f0; font-size: 14px; } .ac-list a:hover, .ac-list a.active { background: #f5f5f5; } /* Layout */ .content { display: flex; gap: 30px; } .sidebar { width: 220px; flex-shrink: 0; } .main { flex: 1; min-width: 0; } /* Facets */ .facet-group h4 { font-size: 13px; text-transform: uppercase; color: #888; margin-bottom: 8px; letter-spacing: 0.5px; } .facet-group { margin-bottom: 24px; } .facet-group ul { list-style: none; } .facet-group li a { display: flex; justify-content: space-between; padding: 5px 0; text-decoration: none; color: #555; font-size: 14px; } .facet-group li a:hover { color: #e8650a; } .facet-group li.active a { font-weight: 700; color: #e8650a; } .facet-count { color: #aaa; font-size: 12px; } /* Results */ .result-info { font-size: 14px; color: #666; margin-bottom: 16px; } .spellcheck { margin-bottom: 16px; font-size: 14px; } .spellcheck a { color: #e8650a; font-weight: 600; text-decoration: none; } .result { background: white; padding: 18px 20px; margin-bottom: 12px; border-radius: 8px; border: 1px solid #e8e8e8; } .result:hover { border-color: #ccc; } .result h3 { font-size: 17px; margin-bottom: 4px; } .result h3 a { color: #1a0dab; text-decoration: none; } .result h3 a:hover { text-decoration: underline; } .result .url { color: #006621; font-size: 13px; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .result .snippet { color: #545454; font-size: 14px; line-height: 1.6; } .result em { font-weight: 700; font-style: normal; color: #333; } .result .meta { margin-top: 6px; font-size: 12px; color: #999; } /* Pagination */ .pagination { display: flex; justify-content: center; gap: 4px; margin: 30px 0; flex-wrap: wrap; } .pagination a, .pagination span { display: inline-block; padding: 8px 14px; font-size: 14px; border: 1px solid #ddd; border-radius: 6px; text-decoration: none; color: #333; } .pagination a:hover { background: #f0f0f0; } .pagination .current { background: #e8650a; color: white; border-color: #e8650a; font-weight: 600; } .no-results { text-align: center; padding: 60px 20px; color: #888; } @media (max-width: 768px) { .content { flex-direction: column; } .sidebar { width: 100%; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>Search</h1> <div class="search-form"> <div class="ac-wrap"> <input type="text" id="q" placeholder="Search..." autocomplete="off"> <div class="ac-list" id="acList"></div> </div> <button onclick="doSearch()">Search</button> </div> </div> <div class="content"> <div class="sidebar" id="facets"></div> <div class="main" id="results"></div> </div> </div> <script> // =========================================================================== // CONFIGURATION - Replace with your Opensolr index details // =========================================================================== var SOLR_HOST = 'https://YOUR_HOST/solr/YOUR_INDEX'; var SOLR_USER = 'opensolr'; var SOLR_PASS = 'YOUR_API_KEY'; var ROWS = 10; var currentQuery = ''; var currentStart = 0; var activeFilters = {}; var acTimer; var qInput = document.getElementById('q'); var acList = document.getElementById('acList'); // Enter key to search qInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { acList.style.display = 'none'; doSearch(); } if (e.key === 'Escape') { acList.style.display = 'none'; } }); // Autocomplete on input qInput.addEventListener('input', function() { clearTimeout(acTimer); var val = this.value.trim(); if (val.length < 2) { acList.style.display = 'none'; return; } acTimer = setTimeout(function() { fetchAutocomplete(val); }, 150); }); // Hide autocomplete on click outside document.addEventListener('click', function(e) { if (!e.target.closest('.ac-wrap')) acList.style.display = 'none'; }); // ---- Autocomplete ---- function fetchAutocomplete(q) { var url = SOLR_HOST + '/autocomplete?' + new URLSearchParams({ q: q, rows: 7, wt: 'json', fl: 'title,uri', qf: 'title_tags_ws tags_ws title_tags tags', defType: 'edismax' }); solrFetch(url).then(function(data) { var docs = data.response.docs; if (!docs.length) { acList.style.display = 'none'; return; } acList.innerHTML = docs.map(function(d) { return '<a href="#" onclick="event.preventDefault(); pickAc(\''+esc(d.title)+'\')">' + esc(d.title) + '</a>'; }).join(''); acList.style.display = 'block'; }); } function pickAc(title) { qInput.value = title; acList.style.display = 'none'; doSearch(); } // ---- Main Search ---- function doSearch(start) { currentQuery = qInput.value.trim(); currentStart = start || 0; if (!currentQuery) return; var params = { q: currentQuery, df: 'title', defType: 'edismax', qf: 'title^5 description^4 uri^0.5 text^1', pf: 'title^10 description^8', mm: '2<-1 5<-2', fl: 'id,uri,title,description,og_image,meta_icon,creation_date,meta_domain,meta_detected_language,score', rows: ROWS, start: currentStart, wt: 'json', hl: 'true', 'hl.fl': 'title,description,text', 'hl.method': 'unified', 'hl.fragsize': 200, 'hl.tag.pre': '<em>', 'hl.tag.post': '</em>', facet: 'true', 'facet.field': 'meta_detected_language', 'facet.mincount': 1, spellcheck: 'true', 'spellcheck.q': currentQuery, 'spellcheck.count': 5, 'spellcheck.collate': 'true', 'spellcheck.maxCollationTries': 15, fq: 'content_type:text*' }; // Apply active filters var fqList = [params.fq]; for (var field in activeFilters) { fqList.push(field + ':' + activeFilters[field]); } var url = SOLR_HOST + '/select?' + buildQuery(params, fqList); solrFetch(url).then(function(data) { render(data); }); } // ---- Render ---- function render(data) { var docs = data.response.docs; var hl = data.highlighting || {}; var total = data.response.numFound; var resultsDiv = document.getElementById('results'); var html = ''; // Spellcheck if (data.spellcheck && data.spellcheck.collations && total < 5) { for (var i = 0; i < data.spellcheck.collations.length; i++) { if (data.spellcheck.collations[i] === 'collation') { var suggestion = data.spellcheck.collations[i + 1]; if (suggestion) { html += '<div class="spellcheck">Did you mean: <a href="#" onclick="event.preventDefault();document.getElementById(\'q\').value=\''+esc(suggestion)+'\';doSearch()">' + esc(suggestion) + '</a>?</div>'; } break; } } } // Result count if (total > 0) { var from = currentStart + 1; var to = Math.min(currentStart + ROWS, total); html += '<div class="result-info">Showing ' + from + '-' + to + ' of ' + total.toLocaleString() + ' results</div>'; } // Results if (docs.length === 0) { html += '<div class="no-results"><h3>No results found</h3><p>Try different keywords or remove some filters.</p></div>'; } docs.forEach(function(doc) { var h = hl[doc.id] || {}; var t = (h.title && h.title[0]) || esc(doc.title || 'Untitled'); var s = (h.description && h.description[0]) || (h.text && h.text[0]) || esc(doc.description || ''); var date = doc.creation_date ? new Date(doc.creation_date).toLocaleDateString() : ''; var domain = doc.meta_domain || ''; html += '<div class="result">' + '<h3><a href="' + esc(doc.uri) + '" target="_blank">' + t + '</a></h3>' + '<div class="url">' + esc(doc.uri) + '</div>' + '<div class="snippet">' + s + '</div>' + '<div class="meta">' + [domain, date].filter(Boolean).join(' · ') + '</div>' + '</div>'; }); // Pagination if (total > ROWS) { var pages = Math.ceil(total / ROWS); var cur = Math.floor(currentStart / ROWS) + 1; html += '<div class="pagination">'; if (cur > 1) html += '<a href="#" onclick="event.preventDefault();doSearch(' + ((cur-2)*ROWS) + ')">«</a>'; var pStart = Math.max(1, cur - 3); var pEnd = Math.min(pages, cur + 3); for (var p = pStart; p <= pEnd; p++) { if (p === cur) html += '<span class="current">' + p + '</span>'; else html += '<a href="#" onclick="event.preventDefault();doSearch(' + ((p-1)*ROWS) + ')">' + p + '</a>'; } if (cur < pages) html += '<a href="#" onclick="event.preventDefault();doSearch(' + (cur*ROWS) + ')">»</a>'; html += '</div>'; } resultsDiv.innerHTML = html; // Facets renderFacets(data); window.scrollTo(0, 0); } function renderFacets(data) { var div = document.getElementById('facets'); if (!data.facet_counts) { div.innerHTML = ''; return; } var langs = data.facet_counts.facet_fields.meta_detected_language || []; if (!langs.length) { div.innerHTML = ''; return; } var langNames = {en:'English',de:'German',fr:'French',es:'Spanish',it:'Italian',nl:'Dutch',pt:'Portuguese',ro:'Romanian',ja:'Japanese',zh:'Chinese',ko:'Korean',ar:'Arabic',ru:'Russian',sv:'Swedish',pl:'Polish',tr:'Turkish',da:'Danish',fi:'Finnish',no:'Norwegian',cs:'Czech'}; var html = '<div class="facet-group"><h4>Language</h4><ul>'; for (var i = 0; i < langs.length; i += 2) { var code = langs[i], count = langs[i+1]; if (!count) continue; var isActive = activeFilters.meta_detected_language === code; html += '<li' + (isActive ? ' class="active"' : '') + '>' + '<a href="#" onclick="event.preventDefault();toggleFilter(\'meta_detected_language\',\' ' + code + ' \')">' + '<span>' + (langNames[code] || code.toUpperCase()) + '</span>' + '<span class="facet-count">' + count + '</span>' + '</a></li>'; } html += '</ul></div>'; if (Object.keys(activeFilters).length) { html += '<div class="facet-group"><a href="#" onclick="event.preventDefault();activeFilters={};doSearch()" style="color:#e8650a;font-size:13px">Clear all filters</a></div>'; } div.innerHTML = html; } function toggleFilter(field, value) { if (activeFilters[field] === value) delete activeFilters[field]; else activeFilters[field] = value; doSearch(); } // ---- Helpers ---- function solrFetch(url) { return fetch(url, { headers: { 'Authorization': 'Basic ' + btoa(SOLR_USER + ':' + SOLR_PASS) } }).then(function(r) { return r.json(); }); } function buildQuery(params, fqList) { var parts = []; for (var k in params) { if (k === 'fq') continue; parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k])); } fqList.forEach(function(f) { parts.push('fq=' + encodeURIComponent(f)); }); return parts.join('&'); } function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } </script> </body> </html>
What This Example Includes
- Search box with Enter key support
- Autocomplete dropdown with debounced requests (150ms) and keyboard/click interaction
- eDisMax search with field weighting (title^5, description^4, text^1) and phrase boosting
- Highlighted results with title, URL, snippet, domain, and date
- Language facet sidebar with click-to-filter and visual active state
- "Clear all filters" link when filters are active
- Spellcheck "Did you mean...?" when few results are found
- Pagination with page numbers and previous/next arrows
- Responsive layout — sidebar collapses below on mobile
- Clean, modern CSS — no external dependencies
To Use It
- Replace
YOUR_HOST,YOUR_INDEX, andYOUR_API_KEYwith your actual Opensolr credentials - Make sure your domain is whitelisted for CORS (contact Opensolr support)
- Save as an HTML file and open in your browser
That is it — a complete search engine in a single file.