Implement a search interface
Based on https://simonwillison.net/2018/Dec/19/fast-autocomplete-search/
This commit is contained in:
parent
998794db4a
commit
e01fbfe90b
|
@ -1,6 +1,7 @@
|
|||
<nav class="menu">
|
||||
<a class="menu-item {% if page.data.route == "blog"%}current{%endif%}" href="/">fnordig</a>
|
||||
<a class="menu-item {% if page.data.route == "posts"%}current{%endif%}" href="/posts/">posts</a>
|
||||
<a class="menu-item {% if page.data.route == "search"%}current{%endif%}" href="/search/">search</a>
|
||||
<a class="menu-item" href="/til/">til</a>
|
||||
<a class="menu-item {% if page.data.route == "about"%}current{%endif%}" href="/about/">about</a>
|
||||
<a class="menu-item {% if page.data.route == "talks"%}current{%endif%}" href="/talks/">talks</a>
|
||||
|
|
111
search.md
Normal file
111
search.md
Normal file
|
@ -0,0 +1,111 @@
|
|||
---
|
||||
permalink: /search
|
||||
title: Search
|
||||
layout: simple.liquid
|
||||
data:
|
||||
route: search
|
||||
---
|
||||
|
||||
<form>
|
||||
<p><input id="searchbox" type="search" placeholder="Search fnordig" style="width: 60%"></p>
|
||||
</form>
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
function debounce(func, wait, immediate) {
|
||||
let timeout;
|
||||
return function() {
|
||||
let context = this, args = arguments;
|
||||
let later = () => {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
let callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
|
||||
const htmlEscape = (s) => s.replace(
|
||||
/>/g, '>'
|
||||
).replace(
|
||||
/</g, '<'
|
||||
).replace(
|
||||
/&/g, '&'
|
||||
).replace(
|
||||
/"/g, '"'
|
||||
).replace(
|
||||
/'/g, '''
|
||||
);
|
||||
|
||||
const highlight = (s) => htmlEscape(s).replace(
|
||||
/b4de2a49c8/g, '<b>'
|
||||
).replace(
|
||||
/8c94a2ed4b/g, '</b>'
|
||||
);
|
||||
|
||||
function permalink(link, ts) {
|
||||
let d = ts.replace(/ ([-+]....)/, "$1").replace(/ /, "T");
|
||||
let date = new Date(d);
|
||||
return link.replace(/\{\{ *year *}}/, date.getFullYear().toString().padStart(4, '0')
|
||||
).replace(/\{\{ *month *}}/, (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
).replace(/\{\{ *day *}}/, date.getDate().toString().padStart(2, '0'));
|
||||
}
|
||||
|
||||
function datefmt(ts) {
|
||||
let d = ts.replace(/ ([-+]....)/, "$1").replace(/ /, "T");
|
||||
let date = new Date(d);
|
||||
|
||||
let year = date.getFullYear().toString().padStart(4, '0');
|
||||
let month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
let day = date.getDate().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Embed the SQL query in a multi-line backtick string:
|
||||
const sql = `select
|
||||
snippet(posts_fts, -1, 'b4de2a49c8', '8c94a2ed4b', '...', 100) as snippet,
|
||||
posts_fts.rank, posts.title, posts.permalink, posts.published_date
|
||||
from posts
|
||||
join posts_fts on posts.rowid = posts_fts.rowid
|
||||
where posts_fts match :search || "*"
|
||||
order by rank limit 10`;
|
||||
|
||||
// Grab a reference to the <input type="search">
|
||||
const searchbox = document.getElementById("searchbox");
|
||||
|
||||
// Used to avoid race-conditions:
|
||||
let requestInFlight = null;
|
||||
|
||||
searchbox.onkeyup = debounce(() => {
|
||||
const q = searchbox.value;
|
||||
// Construct the API URL, using encodeURIComponent() for the parameters
|
||||
const url = (
|
||||
"http://127.0.0.1:8001/blog.json?sql=" +
|
||||
encodeURIComponent(sql) +
|
||||
`&search=${encodeURIComponent(q)}&_shape=array`
|
||||
);
|
||||
// Unique object used just for race-condition comparison
|
||||
let currentRequest = {};
|
||||
requestInFlight = currentRequest;
|
||||
fetch(url).then(r => r.json()).then(d => {
|
||||
if (requestInFlight !== currentRequest) {
|
||||
// Avoid race conditions where a slow request returns
|
||||
// after a faster one.
|
||||
return;
|
||||
}
|
||||
let results = d.map(r => {
|
||||
let link = permalink(r.permalink, r.published_date);
|
||||
return `
|
||||
<div class="result">
|
||||
<h3><a href="${link}">${htmlEscape(r.title)}</a></h3>
|
||||
<p><small>${datefmt(r.published_date)}</small></p>
|
||||
<p>${highlight(r.snippet)}</p>
|
||||
</div>
|
||||
`
|
||||
}).join("");
|
||||
document.getElementById("results").innerHTML = results;
|
||||
});
|
||||
}, 100); // debounce every 100ms
|
||||
</script>
|
Loading…
Reference in a new issue