Complete Search Page: End-to-End Example

Code Examples

One HTML file with a search box, highlighted results, a language facet, Did you mean and pages. It talks only to your own server: the endpoint of the PHP, Node.js or Python example, which holds the keys and runs the hybrid search.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Search</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; color: #111111; background: #ffffff; }
.wrap { max-width: 1000px; margin: 0 auto; padding: 24px 16px; }
form { display: flex; gap: 8px; margin-bottom: 12px; }
input[type=search] { flex: 1; min-width: 0; font-size: 17px; padding: 10px 12px; border: 1px solid #b9b3a9; border-radius: 2px; }
button { font: inherit; cursor: pointer; }
form button { font-size: 16px; padding: 10px 18px; border: 0; border-radius: 2px; background: #c05520; color: #ffffff; }
.layout { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 32px; }
@media (max-width: 720px) { .layout { grid-template-columns: minmax(0, 1fr); } }
.link { border: 0; background: none; padding: 4px 0; color: #c05520; font-size: 15px; text-align: left; }
.link.active { color: #111111; font-weight: 700; }
#facets .link { display: block; }
.result { padding: 14px 0; border-bottom: 1px solid #d9d4cc; }
.result a { font-size: 18px; color: #111111; }
.result .url { font-size: 14px; color: #4a4540; word-break: break-all; }
.result em { font-style: normal; font-weight: 700; }
.pages { display: flex; gap: 16px; align-items: center; margin-top: 16px; }
</style>
</head>
<body>
<div class="wrap">
  <form id="form"><input type="search" id="q" placeholder="Search" autocomplete="off"><button type="submit">Search</button></form>
  <p id="info"></p>
  <p id="didyoumean" hidden>Did you mean <button type="button" class="link" id="suggestion"></button>?</p>
  <div class="layout">
    <aside id="facets"></aside>
    <main><div id="results"></div><div class="pages" id="pages"></div></main>
  </div>
</div>
<script>
var BACKEND = '/search.php';
var ROWS = 10;
var LANGUAGES = { en: 'English', de: 'German', fr: 'French', es: 'Spanish', it: 'Italian', nl: 'Dutch', pt: 'Portuguese', ro: 'Romanian' };
var state = { q: '', page: 1, lang: '' };

function el(tag, text, cls) {
  var e = document.createElement(tag);
  if (text != null) e.textContent = text;
  if (cls) e.className = cls;
  return e;
}

function run() {
  if (!state.q) return;
  var url = BACKEND + '?' + new URLSearchParams({ q: state.q, page: state.page, lang: state.lang });
  fetch(url)
    .then(function (r) { return r.json(); })
    .then(render)
    .catch(function () { document.getElementById('info').textContent = 'Search is not available right now.'; });
}

function render(data) {
  var results = document.getElementById('results');
  results.replaceChildren();
  if (!data.response) {
    document.getElementById('info').textContent = 'Search is not available right now.';
    return;
  }
  var total = data.response.numFound;
  document.getElementById('info').textContent = total + (total === 1 ? ' result' : ' results');
  var hl = data.highlighting || {};
  data.response.docs.forEach(function (doc) {
    var h = hl[doc.id] || {};
    var box = el('div', null, 'result');
    var a = el('a');
    if (/^https?:\/\//i.test(doc.uri || '')) a.href = doc.uri;
    // Highlighted text is HTML-escaped by Solr (hl.encoder=html): only its <em> tags are markup
    if (h.title && h.title[0]) a.innerHTML = h.title[0]; else a.textContent = doc.title || doc.uri || '';
    var snippet = el('p');
    var s = (h.description && h.description[0]) || (h.text && h.text[0]);
    if (s) snippet.innerHTML = s; else snippet.textContent = doc.description || '';
    box.append(a, el('div', doc.uri || '', 'url'), snippet);
    results.appendChild(box);
  });
  renderDidYouMean(data, total);
  renderFacets(data);
  renderPages(total);
}

function renderDidYouMean(data, total) {
  var box = document.getElementById('didyoumean');
  var c = data.spellcheck && data.spellcheck.collations;
  box.hidden = true;
  if (total < 5 && c && c.length >= 2) {
    document.getElementById('suggestion').textContent = c[1];
    box.hidden = false;
  }
}

function renderFacets(data) {
  var aside = document.getElementById('facets');
  aside.replaceChildren();
  var pairs = (data.facet_counts && data.facet_counts.facet_fields.meta_detected_language) || [];
  if (!pairs.length) return;
  aside.appendChild(el('strong', 'Language'));
  for (var i = 0; i < pairs.length; i += 2) {
    var code = String(pairs[i]);
    var b = el('button', (LANGUAGES[code] || code) + ' (' + pairs[i + 1] + ')', 'link' + (state.lang === code ? ' active' : ''));
    b.type = 'button';
    b.dataset.lang = code;
    aside.appendChild(b);
  }
}

function renderPages(total) {
  var nav = document.getElementById('pages');
  nav.replaceChildren();
  var last = Math.min(100, Math.ceil(total / ROWS));
  if (last < 2) return;
  if (state.page > 1) nav.appendChild(pageButton('Previous', state.page - 1));
  nav.appendChild(el('span', 'Page ' + state.page + ' of ' + last));
  if (state.page < last) nav.appendChild(pageButton('Next', state.page + 1));
}

function pageButton(label, page) {
  var b = el('button', label, 'link');
  b.type = 'button';
  b.dataset.page = page;
  return b;
}

document.getElementById('form').addEventListener('submit', function (e) {
  e.preventDefault();
  state = { q: document.getElementById('q').value.trim(), page: 1, lang: '' };
  run();
});
document.getElementById('facets').addEventListener('click', function (e) {
  var lang = e.target.dataset && e.target.dataset.lang;
  if (!lang) return;
  state.lang = state.lang === lang ? '' : lang;
  state.page = 1;
  run();
});
document.getElementById('pages').addEventListener('click', function (e) {
  var page = e.target.dataset && e.target.dataset.page;
  if (!page) return;
  state.page = parseInt(page, 10);
  run();
  window.scrollTo(0, 0);
});
document.getElementById('suggestion').addEventListener('click', function () {
  var text = this.textContent;
  document.getElementById('q').value = text;
  state = { q: text, page: 1, lang: '' };
  run();
});
</script>
</body>
</html>

01 · To use it

01

Put the PHP endpoint on your server as search.php, next to this file, or set BACKEND to the address of the Node.js or Python one.

02

Open the page and search.

02 · Why it is safe

No API key and no password in the page: the browser only calls your endpoint.

Highlighted text goes in as HTML because the endpoint asks Solr for hl.encoder=html, which escapes everything except the <em> marks. Every other value goes in as text.

Links open only http and https addresses.

Suggestions while typing: autocomplete. No page to build at all: put the hosted search page on your site.