@@ -0,0 +1,466 @@ | |||
<!doctype html><!-- This is a valid HTML5 document. --> | |||
<!-- Screen readers, SEO, extensions and so on. --> | |||
<html lang="fr"> | |||
<!-- Has to be within the first 1024 bytes, hence before the `title` element | |||
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset --> | |||
<meta charset="utf-8"> | |||
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 --> | |||
<!-- The viewport meta is quite crowded and we are responsible for that. | |||
See: https://codepen.io/tigt/post/meta-viewport-for-2015 --> | |||
<meta name="viewport" content="width=device-width,initial-scale=1"> | |||
<!-- Required to make a valid HTML5 document. --> | |||
<title>How to create a search page for a static website with vanilla JS (archive) — David Larlet</title> | |||
<meta name="description" content="Publication mise en cache pour en conserver une trace."> | |||
<!-- That good ol' feed, subscribe :). --> | |||
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/"> | |||
<!-- Generated from https://realfavicongenerator.net/ such a mess. --> | |||
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png"> | |||
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png"> | |||
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png"> | |||
<link rel="manifest" href="/static/david/icons2/site.webmanifest"> | |||
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c"> | |||
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico"> | |||
<meta name="msapplication-TileColor" content="#f7f7f7"> | |||
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml"> | |||
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)"> | |||
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)"> | |||
<!-- Documented, feel free to shoot an email. --> | |||
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css"> | |||
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. --> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<script> | |||
function toggleTheme(themeName) { | |||
document.documentElement.classList.toggle( | |||
'forced-dark', | |||
themeName === 'dark' | |||
) | |||
document.documentElement.classList.toggle( | |||
'forced-light', | |||
themeName === 'light' | |||
) | |||
} | |||
const selectedTheme = localStorage.getItem('theme') | |||
if (selectedTheme !== 'undefined') { | |||
toggleTheme(selectedTheme) | |||
} | |||
</script> | |||
<meta name="robots" content="noindex, nofollow"> | |||
<meta content="origin-when-cross-origin" name="referrer"> | |||
<!-- Canonical URL for SEO purposes --> | |||
<link rel="canonical" href="https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/"> | |||
<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all"> | |||
<article> | |||
<header> | |||
<h1>How to create a search page for a static website with vanilla JS</h1> | |||
</header> | |||
<nav> | |||
<p class="center"> | |||
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use> | |||
</svg> Accueil</a> • | |||
<a href="https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/" title="Lien vers le contenu original">Source originale</a> | |||
</p> | |||
</nav> | |||
<hr> | |||
<p>One of the biggest missing features from most static site generators (like <a href="https://gohugo.io/">Hugo</a>, <a href="https://www.11ty.io/">11ty</a>, and <a href="https://jekyllrb.com/">Jekyll</a>, ) is that they lack built-in search.</p> | |||
<p>Database-driven platforms like WordPress make a server call and search the database to find matching content. Static websites have no database to query.</p> | |||
<p>Today, I’m going to share how I built <a href="https://gomakethings.com/search/">the search functionality for my site</a> with vanilla JS. Let’s dig in!</p> | |||
<h2 id="quick-aside-done-for-you-alternative">Quick aside: done-for-you alternative</h2> | |||
<p>If you don’t want to roll-your-own search functionality, <a href="https://www.algolia.com/">Algolia</a> and <a href="https://www.elastic.co/">ElasticSearch</a> are two done-for-you search vendors.</p> | |||
<p>They both offer free tiers, as well as paid versions with more advanced features.</p> | |||
<p>But, because I like to <del>do things the hard way</del> have more control over the user experience, I wrote my own search functionality instead of using one of them.</p> | |||
<h2 id="the-search-form">The Search Form</h2> | |||
<p>My search functionality starts as a progressively enhanced search form.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p"><</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">"https://duckduckgo.com/"</span> <span class="na">method</span><span class="o">=</span><span class="s">"get"</span> <span class="na">id</span><span class="o">=</span><span class="s">"form-search"</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-search"</span><span class="p">></span>Enter your search criteria:<span class="p"></</span><span class="nt">label</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"text"</span> <span class="na">name</span><span class="o">=</span><span class="s">"q"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-search"</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"hidden"</span> <span class="na">name</span><span class="o">=</span><span class="s">"sites"</span> <span class="na">value</span><span class="o">=</span><span class="s">"YourAwesomeWebsite.com"</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">button</span><span class="p">></span>Search<span class="p"></</span><span class="nt">button</span><span class="p">></span> | |||
<span class="p"></</span><span class="nt">form</span><span class="p">></span></code></pre></div> | |||
<p>If the JavaScript fails (or the user tries to search before it loads), this will open up <a href="https://duckduckgo.com/">Duck Duck Go</a> and search for articles only on my site.</p> | |||
<p>Be sure to replace <code>YourAwesomeWebsite.com</code> with the actual URL to your site.</p> | |||
<p>We’ll also add two additional elements to the page. The <code>#search-results</code> element is where we’ll inject the actual search results. The <code>#search-status</code> element is where we’ll display the number of items found.</p> | |||
<p>We want this to announce to screen readers, so we’ll also add the <code>[role="status"]</code> attribute to it.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-status"</span> <span class="na">role</span><span class="o">=</span><span class="s">"status"</span><span class="p">></</span><span class="nt">div</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-results"</span><span class="p">></</span><span class="nt">div</span><span class="p">></span></code></pre></div> | |||
<h2 id="creating-a-search-index">Creating a search index</h2> | |||
<p>In order to search your site, we need to create an index of content.</p> | |||
<p>The process for this varies from one static site generator to another, but the end result is the same. You want to generate an array of all of the searchable content on your site.</p> | |||
<p>Some people create an external JSON file for this, but I prefer to embed it as a JavaScript variable directly on the search page. it looks like this:</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">let</span> <span class="nx">searchIndex</span> <span class="o">=</span> <span class="p">[</span> | |||
<span class="p">{</span> | |||
<span class="nx">title</span><span class="o">:</span> <span class="s2">"My awesome article"</span><span class="p">,</span> | |||
<span class="nx">date</span><span class="o">:</span> <span class="s2">"December 18, 2018"</span><span class="p">,</span> | |||
<span class="nx">url</span><span class="o">:</span> <span class="s2">"https://gomakethings.com/my-awesome-article"</span><span class="p">,</span> | |||
<span class="nx">content</span><span class="o">:</span> <span class="s2">"The full text of the content..."</span><span class="p">,</span> | |||
<span class="nx">summary</span><span class="o">:</span> <span class="s2">"A short summary or preview of the content (can also be a clipped version of the first few sentences)..."</span> | |||
<span class="p">},</span> | |||
<span class="c1">// More content... | |||
</span><span class="c1"></span><span class="p">];</span> | |||
</code></pre></div> | |||
<p>We can use this to both search for articles and generate results on the page.</p> | |||
<h2 id="creating-a-search-function">Creating a search function</h2> | |||
<p>Next, let’s create a function to actually <em>do</em> the searching. This can be <a href="https://gomakethings.com/the-many-ways-to-write-an-immediately-invoked-function-expression-iife-in-javascript/">an IIFE</a> or a named function. We just want a way to <a href="https://gomakethings.com/how-scope-works-in-javascript/">scope our code</a>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="c1">// Code will go here... | |||
</span><span class="c1"></span><span class="p">})();</span> | |||
</code></pre></div> | |||
<p>Next, we need to get the needed elements from the DOM. We can do that with the <code>document.querySelector()</code> method.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="c1">// Get the DOM elements | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#form-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">input</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#input-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">resultList</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-results'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">searchStatus</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-status'</span><span class="p">);</span> | |||
<span class="p">})();</span> | |||
</code></pre></div> | |||
<p>If we can’t find any of them, or if the <code>searchIndex</code> doesn’t exist, we’ll <code>return</code> to stop the function from doing anything else.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="c1">// Get the DOM elements | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#form-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">input</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#input-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">resultList</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-results'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">searchStatus</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-status'</span><span class="p">);</span> | |||
<span class="c1">// Make sure required content exists | |||
</span><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">form</span> <span class="o">||</span> <span class="o">!</span><span class="nx">input</span> <span class="o">||</span> <span class="o">!</span><span class="nx">resultList</span> <span class="o">||</span> <span class="o">!</span><span class="nx">searchStatus</span> <span class="o">||</span> <span class="o">!</span><span class="nx">searchIndex</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> | |||
<span class="p">})();</span> | |||
</code></pre></div> | |||
<h2 id="running-a-search">Running a search</h2> | |||
<p>Next, we need to detect when the user searches for something. To do that, we’ll listen for <code>submit</code> events on the <code>form</code> element.</p> | |||
<p>(<em>The rest of the code all happens inside the IIFE, but I’m sharing just the relevant stuff to make it easier to read.</em>)</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="c1">// Create a submit handler | |||
</span><span class="c1"></span><span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'submit'</span><span class="p">,</span> <span class="nx">submitHandler</span><span class="p">);</span> | |||
</code></pre></div> | |||
<p>In the <code>submitHandler()</code> function, we’ll use the <code>event.preventDefault()</code> method to stop the form from submitting to Duck Duck Go. Then, we’ll pass the <code>input.value</code> into a <code>search()</code> function that will actually look for results.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Handle submit events | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">submitHandler</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span> | |||
<span class="nx">search</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="searching-for-results">Searching for results</h2> | |||
<p>Here’s where stuff gets a bit messy.</p> | |||
<p>Rather than search for complete phrases, we want to look at each word from the search query, and look for it in the titles and content of our articles. We want to ignore case, and we probably also want to ignore common words like <code>a</code>, <code>an</code>, and <code>the</code>.</p> | |||
<p>I use <a href="https://gomakethings.com/converting-strings-to-uppercase-and-lowercase-with-vanilla-javascript/">the <code>String.toLowerCase()</code> method</a> to convert the <code>query</code> to lowercase. Then, I use <a href="https://gomakethings.com/getting-an-array-from-a-string-with-vanilla-js/">the <code>String.split()</code> method</a> to convert it to an array, with each word as its own item.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">regMap</span> <span class="o">=</span> <span class="nx">query</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Next, I created an array of <code>stopWords</code>: words that should be ignored. I found a list on the web, and modified it based on the type of content I have on my site.</p> | |||
<p>For example, I added <code>vanilla</code>, <code>javascript</code>, and <code>js</code> to my list, since almost every article I write includes those words heavily, making them meaningless.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">let</span> <span class="nx">stopWords</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'a'</span><span class="p">,</span> <span class="s1">'an'</span><span class="p">,</span> <span class="s1">'and'</span><span class="p">,</span> <span class="s1">'are'</span><span class="p">,</span> <span class="s1">'aren\'t'</span><span class="p">,</span> <span class="s1">'as'</span><span class="p">,</span> <span class="s1">'by'</span><span class="p">,</span> <span class="s1">'can'</span><span class="p">,</span> <span class="s1">'cannot'</span><span class="p">,</span> <span class="s1">'can\'t'</span><span class="p">,</span> <span class="s1">'could'</span><span class="p">,</span> <span class="s1">'couldn\'t'</span><span class="p">,</span> <span class="s1">'how'</span><span class="p">,</span> <span class="s1">'is'</span><span class="p">,</span> <span class="s1">'isn\'t'</span><span class="p">,</span> <span class="s1">'it'</span><span class="p">,</span> <span class="s1">'its'</span><span class="p">,</span> <span class="s1">'it\'s'</span><span class="p">,</span> <span class="s1">'that'</span><span class="p">,</span> <span class="s1">'the'</span><span class="p">,</span> <span class="s1">'their'</span><span class="p">,</span> <span class="s1">'there'</span><span class="p">,</span> <span class="s1">'they'</span><span class="p">,</span> <span class="s1">'they\'re'</span><span class="p">,</span> <span class="s1">'them'</span><span class="p">,</span> <span class="s1">'to'</span><span class="p">,</span> <span class="s1">'too'</span><span class="p">,</span> <span class="s1">'us'</span><span class="p">,</span> <span class="s1">'very'</span><span class="p">,</span> <span class="s1">'was'</span><span class="p">,</span> <span class="s1">'we'</span><span class="p">,</span> <span class="s1">'well'</span><span class="p">,</span> <span class="s1">'were'</span><span class="p">,</span> <span class="s1">'what'</span><span class="p">,</span> <span class="s1">'whatever'</span><span class="p">,</span> <span class="s1">'when'</span><span class="p">,</span> <span class="s1">'whenever'</span><span class="p">,</span> <span class="s1">'where'</span><span class="p">,</span> <span class="s1">'with'</span><span class="p">,</span> <span class="s1">'would'</span><span class="p">,</span> <span class="s1">'yet'</span><span class="p">,</span> <span class="s1">'you'</span><span class="p">,</span> <span class="s1">'your'</span><span class="p">,</span> <span class="s1">'yours'</span><span class="p">,</span> <span class="s1">'yourself'</span><span class="p">,</span> <span class="s1">'yourselves'</span><span class="p">,</span> <span class="s1">'the'</span><span class="p">,</span> <span class="s1">'vanilla'</span><span class="p">,</span> <span class="s1">'javascript'</span><span class="p">,</span> <span class="s1">'js'</span><span class="p">];</span> | |||
</code></pre></div> | |||
<p>Back in my <code>search()</code> function, I use <a href="https://gomakethings.com/what-array.filter-does-in-vanilla-js/">the <code>Array.filter()</code> method</a> to remove any <code>word</code> that’s an empty string or part of the <code>stopWords</code> array.</p> | |||
<p>I use <a href="https://gomakethings.com/how-to-check-for-an-item-in-an-array-with-vanilla-js/">the <code>Array.includes()</code> method</a> to check if the <code>word</code> is in <code>stopWords</code>.</p> | |||
<p>Finally, I use <a href="https://gomakethings.com/what-array.map-does-in-vanilla-js/">the <code>Array.map()</code> method</a> an <code>new RegExp()</code> constructor to create an array of regex searches from my <code>query</code>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">regMap</span> <span class="o">=</span> <span class="nx">query</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">).</span><span class="nx">filter</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">word</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">return</span> <span class="nx">word</span><span class="p">.</span><span class="nx">length</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">stopWords</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">word</span><span class="p">);</span> | |||
<span class="p">}).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">word</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">return</span> <span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="nx">word</span><span class="p">,</span> <span class="s1">'i'</span><span class="p">);</span> | |||
<span class="p">});</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="doing-the-actual-search">Doing the actual search</h2> | |||
<p>Now that I have my regex patterns all setup, I can actually <em>do</em> the search.</p> | |||
<p>For this, I use <a href="https://gomakethings.com/using-array.reduce-in-vanilla-js/">the <code>Array.reduce()</code> method</a> on my <code>searchIndex</code>. I want to create a new array containing just matching items. I also want to include a <code>priority</code> rating, so that more closing matching items are shown higher in the results.</p> | |||
<p>I pass in an empty array (<code>[]</code>) as my <em>accumulator</em>, which I assign to the <code>results</code> parameter.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Do stuff... | |||
</span><span class="c1"></span> <span class="p">},</span> <span class="p">[]);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Inside the callback function, I create a <code>priority</code> variable with a value of <code>0</code>.</p> | |||
<p>Then, I loop through each item in my <code>regMap</code> using <a href="https://gomakethings.com/the-for...of-loop-in-vanilla-js/">a <code>for...of</code> loop</a>. I use the <code>RegExp.test()</code> method to look for matches in the <code>article.title</code>, and <code>RegExp.match()</code> method to look for matches in the <code>article.content</code>.</p> | |||
<p>I give more weight to the <code>title</code> than content. If there’s a match, I increase the <code>priority</code> by <code>100</code>. For every match in <code>content</code>, I increase the <code>priority</code> by <code>1</code>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Setup priority count | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">priority</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> | |||
<span class="c1">// Assign priority | |||
</span><span class="c1"></span> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">reg</span> <span class="k">of</span> <span class="nx">regMap</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">reg</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">article</span><span class="p">.</span><span class="nx">title</span><span class="p">))</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="mi">100</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="kd">let</span> <span class="nx">occurences</span> <span class="o">=</span> <span class="nx">article</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">reg</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">occurences</span><span class="p">)</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="nx">occurences</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="p">}</span> | |||
<span class="p">},</span> <span class="p">[]);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>If <code>priority</code> is greater than <code>0</code>, I use the <code>Array.push()</code> method to add a new object (<code>{}</code>) to the <code>results</code> array.</p> | |||
<p>I include the <code>priority</code> and <code>article</code> as properties. Then, I <code>return</code> the <code>results</code>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Setup priority count | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">priority</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> | |||
<span class="c1">// Assign priority | |||
</span><span class="c1"></span> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">reg</span> <span class="k">of</span> <span class="nx">regMap</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">reg</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">article</span><span class="p">.</span><span class="nx">title</span><span class="p">))</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="mi">100</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="kd">let</span> <span class="nx">occurences</span> <span class="o">=</span> <span class="nx">article</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">reg</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">occurences</span><span class="p">)</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="nx">occurences</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="p">}</span> | |||
<span class="c1">// If any matches, push to results | |||
</span><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="nx">priority</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">results</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span> | |||
<span class="nx">priority</span><span class="o">:</span> <span class="nx">priority</span><span class="p">,</span> | |||
<span class="nx">article</span><span class="o">:</span> <span class="nx">article</span> | |||
<span class="p">});</span> | |||
<span class="p">}</span> | |||
<span class="k">return</span> <span class="nx">results</span><span class="p">;</span> | |||
<span class="p">},</span> <span class="p">[]);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Finally, I use <a href="https://gomakethings.com/array-sorting-basics-with-vanilla-javascript/">the <code>Array.sort()</code> method</a> to order the <code>results</code> by article priority. Items with the highest <code>priority</code> show up first.</p> | |||
<p>Then, I pass the <code>results</code> into a <code>showResults()</code> method that renders them into the UI.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// ... | |||
</span><span class="c1"></span> <span class="p">},</span> <span class="p">[]).</span><span class="nx">sort</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">article1</span><span class="p">,</span> <span class="nx">article2</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">return</span> <span class="nx">article2</span><span class="p">.</span><span class="nx">priority</span> <span class="o">-</span> <span class="nx">article1</span><span class="p">.</span><span class="nx">priority</span><span class="p">;</span> | |||
<span class="p">});</span> | |||
<span class="c1">// Display the results | |||
</span><span class="c1"></span> <span class="nx">showResults</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="rendering-search-results">Rendering search results</h2> | |||
<p>Inside the <code>showResults()</code> method, I do a quick check to see if their are any results to show.</p> | |||
<p>If there are, I inject a message into the <code>searchStatus</code> element that shares how many matches were found. This also gets read aloud by screen readers.</p> | |||
<p>Then, I use the <code>results</code> to create an HTML string with the <code>title</code> and a link to the article. The appearance of this varies from one site to another, but you can style it however you want.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Show the search results in the UI | |||
</span><span class="cm"> * @param {Array} results The results to display | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">showResults</span> <span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">searchStatus</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="sb">`<p>Found </span><span class="si">${</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="si">}</span><span class="sb"> matching articles</p>`</span><span class="p">;</span> | |||
<span class="nx">resultList</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">myTemplate</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>If there are no <code>results</code>, I clear the <code>resultList</code> element and show a message saying there were no matches.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Show the search results in the UI | |||
</span><span class="cm"> * @param {Array} results The results to display | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">showResults</span> <span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">searchStatus</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="sb">`<p>Found </span><span class="si">${</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="si">}</span><span class="sb"> matching articles</p>`</span><span class="p">;</span> | |||
<span class="nx">resultList</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">myTemplate</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span> | |||
<span class="nx">searchStatus</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s1">'<p>Sorry, no matches were found.</p>'</span><span class="p">;</span> | |||
<span class="nx">resultList</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span> | |||
<span class="p">}</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="what-else">What else?</h2> | |||
<p>Tomorrow, I’ll show you how I update the URL with the search query, and run a search automatically on page load if there’s a query in the URL.</p> | |||
<p>This let’s people bookmark searches.</p> | |||
</article> | |||
<hr> | |||
<footer> | |||
<p> | |||
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use> | |||
</svg> Accueil</a> • | |||
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use> | |||
</svg> Suivre</a> • | |||
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use> | |||
</svg> Pro</a> • | |||
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use> | |||
</svg> Email</a> • | |||
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use> | |||
</svg> Légal</abbr> | |||
</p> | |||
<template id="theme-selector"> | |||
<form> | |||
<fieldset> | |||
<legend><svg class="icon icon-brightness-contrast"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use> | |||
</svg> Thème</legend> | |||
<label> | |||
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto | |||
</label> | |||
<label> | |||
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé | |||
</label> | |||
<label> | |||
<input type="radio" value="light" name="chosen-color-scheme"> Clair | |||
</label> | |||
</fieldset> | |||
</form> | |||
</template> | |||
</footer> | |||
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script> | |||
<script> | |||
function loadThemeForm(templateName) { | |||
const themeSelectorTemplate = document.querySelector(templateName) | |||
const form = themeSelectorTemplate.content.firstElementChild | |||
themeSelectorTemplate.replaceWith(form) | |||
form.addEventListener('change', (e) => { | |||
const chosenColorScheme = e.target.value | |||
localStorage.setItem('theme', chosenColorScheme) | |||
toggleTheme(chosenColorScheme) | |||
}) | |||
const selectedTheme = localStorage.getItem('theme') | |||
if (selectedTheme && selectedTheme !== 'undefined') { | |||
form.querySelector(`[value="${selectedTheme}"]`).checked = true | |||
} | |||
} | |||
const prefersColorSchemeDark = '(prefers-color-scheme: dark)' | |||
window.addEventListener('load', () => { | |||
let hasDarkRules = false | |||
for (const styleSheet of Array.from(document.styleSheets)) { | |||
let mediaRules = [] | |||
for (const cssRule of styleSheet.cssRules) { | |||
if (cssRule.type !== CSSRule.MEDIA_RULE) { | |||
continue | |||
} | |||
// WARNING: Safari does not have/supports `conditionText`. | |||
if (cssRule.conditionText) { | |||
if (cssRule.conditionText !== prefersColorSchemeDark) { | |||
continue | |||
} | |||
} else { | |||
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) { | |||
continue | |||
} | |||
} | |||
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules)) | |||
} | |||
// WARNING: do not try to insert a Rule to a styleSheet you are | |||
// currently iterating on, otherwise the browser will be stuck | |||
// in a infinite loop… | |||
for (const mediaRule of mediaRules) { | |||
styleSheet.insertRule(mediaRule.cssText) | |||
hasDarkRules = true | |||
} | |||
} | |||
if (hasDarkRules) { | |||
loadThemeForm('#theme-selector') | |||
} | |||
}) | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,299 @@ | |||
title: How to create a search page for a static website with vanilla JS | |||
url: https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/ | |||
hash_url: 09da2c716e8bb15e5d22ee071cf39358 | |||
<p>One of the biggest missing features from most static site generators (like <a href="https://gohugo.io/">Hugo</a>, <a href="https://www.11ty.io/">11ty</a>, and <a href="https://jekyllrb.com/">Jekyll</a>, ) is that they lack built-in search.</p> | |||
<p>Database-driven platforms like WordPress make a server call and search the database to find matching content. Static websites have no database to query.</p> | |||
<p>Today, I’m going to share how I built <a href="https://gomakethings.com/search/">the search functionality for my site</a> with vanilla JS. Let’s dig in!</p> | |||
<h2 id="quick-aside-done-for-you-alternative">Quick aside: done-for-you alternative</h2> | |||
<p>If you don’t want to roll-your-own search functionality, <a href="https://www.algolia.com/">Algolia</a> and <a href="https://www.elastic.co/">ElasticSearch</a> are two done-for-you search vendors.</p> | |||
<p>They both offer free tiers, as well as paid versions with more advanced features.</p> | |||
<p>But, because I like to <del>do things the hard way</del> have more control over the user experience, I wrote my own search functionality instead of using one of them.</p> | |||
<h2 id="the-search-form">The Search Form</h2> | |||
<p>My search functionality starts as a progressively enhanced search form.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p"><</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">"https://duckduckgo.com/"</span> <span class="na">method</span><span class="o">=</span><span class="s">"get"</span> <span class="na">id</span><span class="o">=</span><span class="s">"form-search"</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-search"</span><span class="p">></span>Enter your search criteria:<span class="p"></</span><span class="nt">label</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"text"</span> <span class="na">name</span><span class="o">=</span><span class="s">"q"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-search"</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"hidden"</span> <span class="na">name</span><span class="o">=</span><span class="s">"sites"</span> <span class="na">value</span><span class="o">=</span><span class="s">"YourAwesomeWebsite.com"</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">button</span><span class="p">></span>Search<span class="p"></</span><span class="nt">button</span><span class="p">></span> | |||
<span class="p"></</span><span class="nt">form</span><span class="p">></span></code></pre></div> | |||
<p>If the JavaScript fails (or the user tries to search before it loads), this will open up <a href="https://duckduckgo.com/">Duck Duck Go</a> and search for articles only on my site.</p> | |||
<p>Be sure to replace <code>YourAwesomeWebsite.com</code> with the actual URL to your site.</p> | |||
<p>We’ll also add two additional elements to the page. The <code>#search-results</code> element is where we’ll inject the actual search results. The <code>#search-status</code> element is where we’ll display the number of items found.</p> | |||
<p>We want this to announce to screen readers, so we’ll also add the <code>[role="status"]</code> attribute to it.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-status"</span> <span class="na">role</span><span class="o">=</span><span class="s">"status"</span><span class="p">></</span><span class="nt">div</span><span class="p">></span> | |||
<span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-results"</span><span class="p">></</span><span class="nt">div</span><span class="p">></span></code></pre></div> | |||
<h2 id="creating-a-search-index">Creating a search index</h2> | |||
<p>In order to search your site, we need to create an index of content.</p> | |||
<p>The process for this varies from one static site generator to another, but the end result is the same. You want to generate an array of all of the searchable content on your site.</p> | |||
<p>Some people create an external JSON file for this, but I prefer to embed it as a JavaScript variable directly on the search page. it looks like this:</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">let</span> <span class="nx">searchIndex</span> <span class="o">=</span> <span class="p">[</span> | |||
<span class="p">{</span> | |||
<span class="nx">title</span><span class="o">:</span> <span class="s2">"My awesome article"</span><span class="p">,</span> | |||
<span class="nx">date</span><span class="o">:</span> <span class="s2">"December 18, 2018"</span><span class="p">,</span> | |||
<span class="nx">url</span><span class="o">:</span> <span class="s2">"https://gomakethings.com/my-awesome-article"</span><span class="p">,</span> | |||
<span class="nx">content</span><span class="o">:</span> <span class="s2">"The full text of the content..."</span><span class="p">,</span> | |||
<span class="nx">summary</span><span class="o">:</span> <span class="s2">"A short summary or preview of the content (can also be a clipped version of the first few sentences)..."</span> | |||
<span class="p">},</span> | |||
<span class="c1">// More content... | |||
</span><span class="c1"></span><span class="p">];</span> | |||
</code></pre></div> | |||
<p>We can use this to both search for articles and generate results on the page.</p> | |||
<h2 id="creating-a-search-function">Creating a search function</h2> | |||
<p>Next, let’s create a function to actually <em>do</em> the searching. This can be <a href="https://gomakethings.com/the-many-ways-to-write-an-immediately-invoked-function-expression-iife-in-javascript/">an IIFE</a> or a named function. We just want a way to <a href="https://gomakethings.com/how-scope-works-in-javascript/">scope our code</a>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="c1">// Code will go here... | |||
</span><span class="c1"></span><span class="p">})();</span> | |||
</code></pre></div> | |||
<p>Next, we need to get the needed elements from the DOM. We can do that with the <code>document.querySelector()</code> method.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="c1">// Get the DOM elements | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#form-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">input</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#input-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">resultList</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-results'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">searchStatus</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-status'</span><span class="p">);</span> | |||
<span class="p">})();</span> | |||
</code></pre></div> | |||
<p>If we can’t find any of them, or if the <code>searchIndex</code> doesn’t exist, we’ll <code>return</code> to stop the function from doing anything else.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="c1">// Get the DOM elements | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#form-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">input</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#input-search'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">resultList</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-results'</span><span class="p">);</span> | |||
<span class="kd">let</span> <span class="nx">searchStatus</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#search-status'</span><span class="p">);</span> | |||
<span class="c1">// Make sure required content exists | |||
</span><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">form</span> <span class="o">||</span> <span class="o">!</span><span class="nx">input</span> <span class="o">||</span> <span class="o">!</span><span class="nx">resultList</span> <span class="o">||</span> <span class="o">!</span><span class="nx">searchStatus</span> <span class="o">||</span> <span class="o">!</span><span class="nx">searchIndex</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> | |||
<span class="p">})();</span> | |||
</code></pre></div> | |||
<h2 id="running-a-search">Running a search</h2> | |||
<p>Next, we need to detect when the user searches for something. To do that, we’ll listen for <code>submit</code> events on the <code>form</code> element.</p> | |||
<p>(<em>The rest of the code all happens inside the IIFE, but I’m sharing just the relevant stuff to make it easier to read.</em>)</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="c1">// Create a submit handler | |||
</span><span class="c1"></span><span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'submit'</span><span class="p">,</span> <span class="nx">submitHandler</span><span class="p">);</span> | |||
</code></pre></div> | |||
<p>In the <code>submitHandler()</code> function, we’ll use the <code>event.preventDefault()</code> method to stop the form from submitting to Duck Duck Go. Then, we’ll pass the <code>input.value</code> into a <code>search()</code> function that will actually look for results.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Handle submit events | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">submitHandler</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span> | |||
<span class="nx">search</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="searching-for-results">Searching for results</h2> | |||
<p>Here’s where stuff gets a bit messy.</p> | |||
<p>Rather than search for complete phrases, we want to look at each word from the search query, and look for it in the titles and content of our articles. We want to ignore case, and we probably also want to ignore common words like <code>a</code>, <code>an</code>, and <code>the</code>.</p> | |||
<p>I use <a href="https://gomakethings.com/converting-strings-to-uppercase-and-lowercase-with-vanilla-javascript/">the <code>String.toLowerCase()</code> method</a> to convert the <code>query</code> to lowercase. Then, I use <a href="https://gomakethings.com/getting-an-array-from-a-string-with-vanilla-js/">the <code>String.split()</code> method</a> to convert it to an array, with each word as its own item.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">regMap</span> <span class="o">=</span> <span class="nx">query</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Next, I created an array of <code>stopWords</code>: words that should be ignored. I found a list on the web, and modified it based on the type of content I have on my site.</p> | |||
<p>For example, I added <code>vanilla</code>, <code>javascript</code>, and <code>js</code> to my list, since almost every article I write includes those words heavily, making them meaningless.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">let</span> <span class="nx">stopWords</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'a'</span><span class="p">,</span> <span class="s1">'an'</span><span class="p">,</span> <span class="s1">'and'</span><span class="p">,</span> <span class="s1">'are'</span><span class="p">,</span> <span class="s1">'aren\'t'</span><span class="p">,</span> <span class="s1">'as'</span><span class="p">,</span> <span class="s1">'by'</span><span class="p">,</span> <span class="s1">'can'</span><span class="p">,</span> <span class="s1">'cannot'</span><span class="p">,</span> <span class="s1">'can\'t'</span><span class="p">,</span> <span class="s1">'could'</span><span class="p">,</span> <span class="s1">'couldn\'t'</span><span class="p">,</span> <span class="s1">'how'</span><span class="p">,</span> <span class="s1">'is'</span><span class="p">,</span> <span class="s1">'isn\'t'</span><span class="p">,</span> <span class="s1">'it'</span><span class="p">,</span> <span class="s1">'its'</span><span class="p">,</span> <span class="s1">'it\'s'</span><span class="p">,</span> <span class="s1">'that'</span><span class="p">,</span> <span class="s1">'the'</span><span class="p">,</span> <span class="s1">'their'</span><span class="p">,</span> <span class="s1">'there'</span><span class="p">,</span> <span class="s1">'they'</span><span class="p">,</span> <span class="s1">'they\'re'</span><span class="p">,</span> <span class="s1">'them'</span><span class="p">,</span> <span class="s1">'to'</span><span class="p">,</span> <span class="s1">'too'</span><span class="p">,</span> <span class="s1">'us'</span><span class="p">,</span> <span class="s1">'very'</span><span class="p">,</span> <span class="s1">'was'</span><span class="p">,</span> <span class="s1">'we'</span><span class="p">,</span> <span class="s1">'well'</span><span class="p">,</span> <span class="s1">'were'</span><span class="p">,</span> <span class="s1">'what'</span><span class="p">,</span> <span class="s1">'whatever'</span><span class="p">,</span> <span class="s1">'when'</span><span class="p">,</span> <span class="s1">'whenever'</span><span class="p">,</span> <span class="s1">'where'</span><span class="p">,</span> <span class="s1">'with'</span><span class="p">,</span> <span class="s1">'would'</span><span class="p">,</span> <span class="s1">'yet'</span><span class="p">,</span> <span class="s1">'you'</span><span class="p">,</span> <span class="s1">'your'</span><span class="p">,</span> <span class="s1">'yours'</span><span class="p">,</span> <span class="s1">'yourself'</span><span class="p">,</span> <span class="s1">'yourselves'</span><span class="p">,</span> <span class="s1">'the'</span><span class="p">,</span> <span class="s1">'vanilla'</span><span class="p">,</span> <span class="s1">'javascript'</span><span class="p">,</span> <span class="s1">'js'</span><span class="p">];</span> | |||
</code></pre></div> | |||
<p>Back in my <code>search()</code> function, I use <a href="https://gomakethings.com/what-array.filter-does-in-vanilla-js/">the <code>Array.filter()</code> method</a> to remove any <code>word</code> that’s an empty string or part of the <code>stopWords</code> array.</p> | |||
<p>I use <a href="https://gomakethings.com/how-to-check-for-an-item-in-an-array-with-vanilla-js/">the <code>Array.includes()</code> method</a> to check if the <code>word</code> is in <code>stopWords</code>.</p> | |||
<p>Finally, I use <a href="https://gomakethings.com/what-array.map-does-in-vanilla-js/">the <code>Array.map()</code> method</a> an <code>new RegExp()</code> constructor to create an array of regex searches from my <code>query</code>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">regMap</span> <span class="o">=</span> <span class="nx">query</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">).</span><span class="nx">filter</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">word</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">return</span> <span class="nx">word</span><span class="p">.</span><span class="nx">length</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">stopWords</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">word</span><span class="p">);</span> | |||
<span class="p">}).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">word</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">return</span> <span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="nx">word</span><span class="p">,</span> <span class="s1">'i'</span><span class="p">);</span> | |||
<span class="p">});</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="doing-the-actual-search">Doing the actual search</h2> | |||
<p>Now that I have my regex patterns all setup, I can actually <em>do</em> the search.</p> | |||
<p>For this, I use <a href="https://gomakethings.com/using-array.reduce-in-vanilla-js/">the <code>Array.reduce()</code> method</a> on my <code>searchIndex</code>. I want to create a new array containing just matching items. I also want to include a <code>priority</code> rating, so that more closing matching items are shown higher in the results.</p> | |||
<p>I pass in an empty array (<code>[]</code>) as my <em>accumulator</em>, which I assign to the <code>results</code> parameter.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Do stuff... | |||
</span><span class="c1"></span> <span class="p">},</span> <span class="p">[]);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Inside the callback function, I create a <code>priority</code> variable with a value of <code>0</code>.</p> | |||
<p>Then, I loop through each item in my <code>regMap</code> using <a href="https://gomakethings.com/the-for...of-loop-in-vanilla-js/">a <code>for...of</code> loop</a>. I use the <code>RegExp.test()</code> method to look for matches in the <code>article.title</code>, and <code>RegExp.match()</code> method to look for matches in the <code>article.content</code>.</p> | |||
<p>I give more weight to the <code>title</code> than content. If there’s a match, I increase the <code>priority</code> by <code>100</code>. For every match in <code>content</code>, I increase the <code>priority</code> by <code>1</code>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Setup priority count | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">priority</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> | |||
<span class="c1">// Assign priority | |||
</span><span class="c1"></span> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">reg</span> <span class="k">of</span> <span class="nx">regMap</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">reg</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">article</span><span class="p">.</span><span class="nx">title</span><span class="p">))</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="mi">100</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="kd">let</span> <span class="nx">occurences</span> <span class="o">=</span> <span class="nx">article</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">reg</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">occurences</span><span class="p">)</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="nx">occurences</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="p">}</span> | |||
<span class="p">},</span> <span class="p">[]);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>If <code>priority</code> is greater than <code>0</code>, I use the <code>Array.push()</code> method to add a new object (<code>{}</code>) to the <code>results</code> array.</p> | |||
<p>I include the <code>priority</code> and <code>article</code> as properties. Then, I <code>return</code> the <code>results</code>.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Setup priority count | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">priority</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> | |||
<span class="c1">// Assign priority | |||
</span><span class="c1"></span> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">reg</span> <span class="k">of</span> <span class="nx">regMap</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">reg</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">article</span><span class="p">.</span><span class="nx">title</span><span class="p">))</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="mi">100</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="kd">let</span> <span class="nx">occurences</span> <span class="o">=</span> <span class="nx">article</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">reg</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">occurences</span><span class="p">)</span> <span class="p">{</span> <span class="nx">priority</span> <span class="o">+=</span> <span class="nx">occurences</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="p">}</span> | |||
<span class="p">}</span> | |||
<span class="c1">// If any matches, push to results | |||
</span><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="nx">priority</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">results</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span> | |||
<span class="nx">priority</span><span class="o">:</span> <span class="nx">priority</span><span class="p">,</span> | |||
<span class="nx">article</span><span class="o">:</span> <span class="nx">article</span> | |||
<span class="p">});</span> | |||
<span class="p">}</span> | |||
<span class="k">return</span> <span class="nx">results</span><span class="p">;</span> | |||
<span class="p">},</span> <span class="p">[]);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Finally, I use <a href="https://gomakethings.com/array-sorting-basics-with-vanilla-javascript/">the <code>Array.sort()</code> method</a> to order the <code>results</code> by article priority. Items with the highest <code>priority</code> show up first.</p> | |||
<p>Then, I pass the <code>results</code> into a <code>showResults()</code> method that renders them into the UI.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create a regex for each query | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Get and sort the results | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">searchIndex</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="nx">article</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// ... | |||
</span><span class="c1"></span> <span class="p">},</span> <span class="p">[]).</span><span class="nx">sort</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">article1</span><span class="p">,</span> <span class="nx">article2</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">return</span> <span class="nx">article2</span><span class="p">.</span><span class="nx">priority</span> <span class="o">-</span> <span class="nx">article1</span><span class="p">.</span><span class="nx">priority</span><span class="p">;</span> | |||
<span class="p">});</span> | |||
<span class="c1">// Display the results | |||
</span><span class="c1"></span> <span class="nx">showResults</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="rendering-search-results">Rendering search results</h2> | |||
<p>Inside the <code>showResults()</code> method, I do a quick check to see if their are any results to show.</p> | |||
<p>If there are, I inject a message into the <code>searchStatus</code> element that shares how many matches were found. This also gets read aloud by screen readers.</p> | |||
<p>Then, I use the <code>results</code> to create an HTML string with the <code>title</code> and a link to the article. The appearance of this varies from one site to another, but you can style it however you want.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Show the search results in the UI | |||
</span><span class="cm"> * @param {Array} results The results to display | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">showResults</span> <span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">searchStatus</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="sb">`<p>Found </span><span class="si">${</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="si">}</span><span class="sb"> matching articles</p>`</span><span class="p">;</span> | |||
<span class="nx">resultList</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">myTemplate</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>If there are no <code>results</code>, I clear the <code>resultList</code> element and show a message saying there were no matches.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Show the search results in the UI | |||
</span><span class="cm"> * @param {Array} results The results to display | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">showResults</span> <span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="nx">searchStatus</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="sb">`<p>Found </span><span class="si">${</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="si">}</span><span class="sb"> matching articles</p>`</span><span class="p">;</span> | |||
<span class="nx">resultList</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">myTemplate</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span> | |||
<span class="nx">searchStatus</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s1">'<p>Sorry, no matches were found.</p>'</span><span class="p">;</span> | |||
<span class="nx">resultList</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span> | |||
<span class="p">}</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="what-else">What else?</h2> | |||
<p>Tomorrow, I’ll show you how I update the URL with the search query, and run a search automatically on page load if there’s a query in the URL.</p> | |||
<p>This let’s people bookmark searches.</p> |
@@ -0,0 +1,275 @@ | |||
<!doctype html><!-- This is a valid HTML5 document. --> | |||
<!-- Screen readers, SEO, extensions and so on. --> | |||
<html lang="fr"> | |||
<!-- Has to be within the first 1024 bytes, hence before the `title` element | |||
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset --> | |||
<meta charset="utf-8"> | |||
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 --> | |||
<!-- The viewport meta is quite crowded and we are responsible for that. | |||
See: https://codepen.io/tigt/post/meta-viewport-for-2015 --> | |||
<meta name="viewport" content="width=device-width,initial-scale=1"> | |||
<!-- Required to make a valid HTML5 document. --> | |||
<title>How to update the URL of a page without causing a reload using vanilla JavaScript (archive) — David Larlet</title> | |||
<meta name="description" content="Publication mise en cache pour en conserver une trace."> | |||
<!-- That good ol' feed, subscribe :). --> | |||
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/"> | |||
<!-- Generated from https://realfavicongenerator.net/ such a mess. --> | |||
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png"> | |||
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png"> | |||
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png"> | |||
<link rel="manifest" href="/static/david/icons2/site.webmanifest"> | |||
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c"> | |||
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico"> | |||
<meta name="msapplication-TileColor" content="#f7f7f7"> | |||
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml"> | |||
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)"> | |||
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)"> | |||
<!-- Documented, feel free to shoot an email. --> | |||
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css"> | |||
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. --> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<script> | |||
function toggleTheme(themeName) { | |||
document.documentElement.classList.toggle( | |||
'forced-dark', | |||
themeName === 'dark' | |||
) | |||
document.documentElement.classList.toggle( | |||
'forced-light', | |||
themeName === 'light' | |||
) | |||
} | |||
const selectedTheme = localStorage.getItem('theme') | |||
if (selectedTheme !== 'undefined') { | |||
toggleTheme(selectedTheme) | |||
} | |||
</script> | |||
<meta name="robots" content="noindex, nofollow"> | |||
<meta content="origin-when-cross-origin" name="referrer"> | |||
<!-- Canonical URL for SEO purposes --> | |||
<link rel="canonical" href="https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/"> | |||
<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all"> | |||
<article> | |||
<header> | |||
<h1>How to update the URL of a page without causing a reload using vanilla JavaScript</h1> | |||
</header> | |||
<nav> | |||
<p class="center"> | |||
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use> | |||
</svg> Accueil</a> • | |||
<a href="https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/" title="Lien vers le contenu original">Source originale</a> | |||
</p> | |||
</nav> | |||
<hr> | |||
<p>Yesterday, we looked at <a href="https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/">how to build a vanilla JavaScript search feature for a static website</a>. At the end, I mentioned…</p> | |||
<blockquote> | |||
<p>Tomorrow, I’ll show you how I update the URL with the search query, and run a search automatically on page load if there’s a query in the URL.</p> | |||
</blockquote> | |||
<p>Well, today is tomorrow, so let’s dig in!</p> | |||
<p><em><strong>Note:</strong> If you haven’t yet, you should probably read yesterday’s post first, or today’s won’t make much sense.</em></p> | |||
<h2 id="updating-the-url">Updating the URL</h2> | |||
<p>In our <code>search()</code> function, we create an array of regex patterns, get an array of matching items (sorted by how many matches they have), and then render them into the UI.</p> | |||
<p>Let’s create another function, <code>updateURL()</code>, to update the URL for us. We’ll pass in the search <code>query</code> as an argument.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Display the results | |||
</span><span class="c1"></span> <span class="nx">showResults</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="c1">// Update the URL | |||
</span><span class="c1"></span> <span class="nx">updateURL</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>We’re going to use <a href="https://gomakethings.com/how-to-update-the-browser-url-without-refreshing-the-page-using-the-vanilla-js-history-api/">the <code>history.pushState()</code> method</a> to update our URL.</p> | |||
<p>This creates a new entry in the browser’s history (and updates the URL) <em>without</em> causing the page to reload. It accepts three arguments: the browser <code>state</code>, a <code>title</code> to use in the <code>document</code>, and the <code>url</code>.</p> | |||
<p>We’ll use the current <code>history.state</code>, no need to replace anything. We’ll also use the current <code>document.title</code>.</p> | |||
<p>For the <code>url</code>, we’ll combine the <code>location.origin</code> and <code>location.pathname</code>, then append the <code>?s</code> query string parameter, and use the <code>query</code> for its value. We’ll pass the <code>query</code> into the <code>encodeURI()</code> method to encode it.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Update the URL with a query string for the search string | |||
</span><span class="cm"> * @param {String} query The search query | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">updateURL</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create the properties | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">history</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span> | |||
<span class="kd">let</span> <span class="nx">title</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">title</span><span class="p">;</span> | |||
<span class="kd">let</span> <span class="nx">url</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span> <span class="o">+</span> <span class="s1">'?s='</span> <span class="o">+</span> <span class="nb">encodeURI</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Finally, we can pass all three into the <code>history.pushState()</code> method to update the URL.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Update the URL with a query string for the search string | |||
</span><span class="cm"> * @param {String} query The search query | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">updateURL</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create the properties | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Update the URL | |||
</span><span class="c1"></span> <span class="nx">history</span><span class="p">.</span><span class="nx">pushState</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">title</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="running-a-search-on-page-load">Running a search on page load</h2> | |||
<p>If the URL has an <code>s</code> query string parameter when the page loads, we should also run a search immediately. This lets users bookmark search pages for later.</p> | |||
<p>First, we’ll create an <code>onload()</code> function to run immediately with the script.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="c1">// Create a submit handler | |||
</span><span class="c1"></span><span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'submit'</span><span class="p">,</span> <span class="nx">submitHandler</span><span class="p">);</span> | |||
<span class="c1">// Check for query strings onload | |||
</span><span class="c1"></span><span class="nx">onload</span><span class="p">();</span> | |||
</code></pre></div> | |||
<p>We’ll use <a href="https://gomakethings.com/getting-values-from-a-url-with-vanilla-js/">the <code>new URLSearchParams()</code> constructor</a> to create a <code>URLSearchParams</code> object from the <code>location.search</code> property.</p> | |||
<p>Then, we’ll use the <code>URLSearchParams.get()</code> method to look for a query string parameter with a key of <code>s</code>.</p> | |||
<p>If one is <em>not</em> found, we’ll use the <code>return</code> operator to end our function.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * If there's a query string search term, search it on page load | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">onload</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="kd">let</span> <span class="nx">query</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">).</span><span class="nx">get</span><span class="p">(</span><span class="s1">'s'</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">query</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>If a <code>query</code> exists, we’ll update the <code>input.value</code> property with it so that the search field contains the search <code>query</code>. Then, we’ll pass the <code>query</code> into the <code>search()</code> function to run a search.</p> | |||
<p>The <code>URLSearchParams.get()</code> method automatically decodes the parameter for us, so we don’t need to worry about that.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * If there's a query string search term, search it on page load | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">onload</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="kd">let</span> <span class="nx">query</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">).</span><span class="nx">get</span><span class="p">(</span><span class="s1">'s'</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">query</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> | |||
<span class="nx">input</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">query</span><span class="p">;</span> | |||
<span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Now, when someone reloads or revists a search page, a new search will automatically run.</p> | |||
</article> | |||
<hr> | |||
<footer> | |||
<p> | |||
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use> | |||
</svg> Accueil</a> • | |||
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use> | |||
</svg> Suivre</a> • | |||
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use> | |||
</svg> Pro</a> • | |||
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use> | |||
</svg> Email</a> • | |||
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use> | |||
</svg> Légal</abbr> | |||
</p> | |||
<template id="theme-selector"> | |||
<form> | |||
<fieldset> | |||
<legend><svg class="icon icon-brightness-contrast"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use> | |||
</svg> Thème</legend> | |||
<label> | |||
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto | |||
</label> | |||
<label> | |||
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé | |||
</label> | |||
<label> | |||
<input type="radio" value="light" name="chosen-color-scheme"> Clair | |||
</label> | |||
</fieldset> | |||
</form> | |||
</template> | |||
</footer> | |||
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script> | |||
<script> | |||
function loadThemeForm(templateName) { | |||
const themeSelectorTemplate = document.querySelector(templateName) | |||
const form = themeSelectorTemplate.content.firstElementChild | |||
themeSelectorTemplate.replaceWith(form) | |||
form.addEventListener('change', (e) => { | |||
const chosenColorScheme = e.target.value | |||
localStorage.setItem('theme', chosenColorScheme) | |||
toggleTheme(chosenColorScheme) | |||
}) | |||
const selectedTheme = localStorage.getItem('theme') | |||
if (selectedTheme && selectedTheme !== 'undefined') { | |||
form.querySelector(`[value="${selectedTheme}"]`).checked = true | |||
} | |||
} | |||
const prefersColorSchemeDark = '(prefers-color-scheme: dark)' | |||
window.addEventListener('load', () => { | |||
let hasDarkRules = false | |||
for (const styleSheet of Array.from(document.styleSheets)) { | |||
let mediaRules = [] | |||
for (const cssRule of styleSheet.cssRules) { | |||
if (cssRule.type !== CSSRule.MEDIA_RULE) { | |||
continue | |||
} | |||
// WARNING: Safari does not have/supports `conditionText`. | |||
if (cssRule.conditionText) { | |||
if (cssRule.conditionText !== prefersColorSchemeDark) { | |||
continue | |||
} | |||
} else { | |||
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) { | |||
continue | |||
} | |||
} | |||
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules)) | |||
} | |||
// WARNING: do not try to insert a Rule to a styleSheet you are | |||
// currently iterating on, otherwise the browser will be stuck | |||
// in a infinite loop… | |||
for (const mediaRule of mediaRules) { | |||
styleSheet.insertRule(mediaRule.cssText) | |||
hasDarkRules = true | |||
} | |||
} | |||
if (hasDarkRules) { | |||
loadThemeForm('#theme-selector') | |||
} | |||
}) | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,108 @@ | |||
title: How to update the URL of a page without causing a reload using vanilla JavaScript | |||
url: https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/ | |||
hash_url: 2c67b87e1b880952bb277fc429cb8bf5 | |||
<p>Yesterday, we looked at <a href="https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/">how to build a vanilla JavaScript search feature for a static website</a>. At the end, I mentioned…</p> | |||
<blockquote> | |||
<p>Tomorrow, I’ll show you how I update the URL with the search query, and run a search automatically on page load if there’s a query in the URL.</p> | |||
</blockquote> | |||
<p>Well, today is tomorrow, so let’s dig in!</p> | |||
<p><em><strong>Note:</strong> If you haven’t yet, you should probably read yesterday’s post first, or today’s won’t make much sense.</em></p> | |||
<h2 id="updating-the-url">Updating the URL</h2> | |||
<p>In our <code>search()</code> function, we create an array of regex patterns, get an array of matching items (sorted by how many matches they have), and then render them into the UI.</p> | |||
<p>Let’s create another function, <code>updateURL()</code>, to update the URL for us. We’ll pass in the search <code>query</code> as an argument.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Search for matches | |||
</span><span class="cm"> * @param {String} query The term to search for | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">search</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Display the results | |||
</span><span class="c1"></span> <span class="nx">showResults</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span> | |||
<span class="c1">// Update the URL | |||
</span><span class="c1"></span> <span class="nx">updateURL</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>We’re going to use <a href="https://gomakethings.com/how-to-update-the-browser-url-without-refreshing-the-page-using-the-vanilla-js-history-api/">the <code>history.pushState()</code> method</a> to update our URL.</p> | |||
<p>This creates a new entry in the browser’s history (and updates the URL) <em>without</em> causing the page to reload. It accepts three arguments: the browser <code>state</code>, a <code>title</code> to use in the <code>document</code>, and the <code>url</code>.</p> | |||
<p>We’ll use the current <code>history.state</code>, no need to replace anything. We’ll also use the current <code>document.title</code>.</p> | |||
<p>For the <code>url</code>, we’ll combine the <code>location.origin</code> and <code>location.pathname</code>, then append the <code>?s</code> query string parameter, and use the <code>query</code> for its value. We’ll pass the <code>query</code> into the <code>encodeURI()</code> method to encode it.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Update the URL with a query string for the search string | |||
</span><span class="cm"> * @param {String} query The search query | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">updateURL</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create the properties | |||
</span><span class="c1"></span> <span class="kd">let</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">history</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span> | |||
<span class="kd">let</span> <span class="nx">title</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">title</span><span class="p">;</span> | |||
<span class="kd">let</span> <span class="nx">url</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span> <span class="o">+</span> <span class="s1">'?s='</span> <span class="o">+</span> <span class="nb">encodeURI</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Finally, we can pass all three into the <code>history.pushState()</code> method to update the URL.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * Update the URL with a query string for the search string | |||
</span><span class="cm"> * @param {String} query The search query | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">updateURL</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span> | |||
<span class="c1">// Create the properties | |||
</span><span class="c1"></span> <span class="c1">// ... | |||
</span><span class="c1"></span> | |||
<span class="c1">// Update the URL | |||
</span><span class="c1"></span> <span class="nx">history</span><span class="p">.</span><span class="nx">pushState</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">title</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<h2 id="running-a-search-on-page-load">Running a search on page load</h2> | |||
<p>If the URL has an <code>s</code> query string parameter when the page loads, we should also run a search immediately. This lets users bookmark search pages for later.</p> | |||
<p>First, we’ll create an <code>onload()</code> function to run immediately with the script.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="c1">// Create a submit handler | |||
</span><span class="c1"></span><span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'submit'</span><span class="p">,</span> <span class="nx">submitHandler</span><span class="p">);</span> | |||
<span class="c1">// Check for query strings onload | |||
</span><span class="c1"></span><span class="nx">onload</span><span class="p">();</span> | |||
</code></pre></div> | |||
<p>We’ll use <a href="https://gomakethings.com/getting-values-from-a-url-with-vanilla-js/">the <code>new URLSearchParams()</code> constructor</a> to create a <code>URLSearchParams</code> object from the <code>location.search</code> property.</p> | |||
<p>Then, we’ll use the <code>URLSearchParams.get()</code> method to look for a query string parameter with a key of <code>s</code>.</p> | |||
<p>If one is <em>not</em> found, we’ll use the <code>return</code> operator to end our function.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * If there's a query string search term, search it on page load | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">onload</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="kd">let</span> <span class="nx">query</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">).</span><span class="nx">get</span><span class="p">(</span><span class="s1">'s'</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">query</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>If a <code>query</code> exists, we’ll update the <code>input.value</code> property with it so that the search field contains the search <code>query</code>. Then, we’ll pass the <code>query</code> into the <code>search()</code> function to run a search.</p> | |||
<p>The <code>URLSearchParams.get()</code> method automatically decodes the parameter for us, so we don’t need to worry about that.</p> | |||
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="cm">/** | |||
</span><span class="cm"> * If there's a query string search term, search it on page load | |||
</span><span class="cm"> */</span> | |||
<span class="kd">function</span> <span class="nx">onload</span> <span class="p">()</span> <span class="p">{</span> | |||
<span class="kd">let</span> <span class="nx">query</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">).</span><span class="nx">get</span><span class="p">(</span><span class="s1">'s'</span><span class="p">);</span> | |||
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">query</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> | |||
<span class="nx">input</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">query</span><span class="p">;</span> | |||
<span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span> | |||
<span class="p">}</span> | |||
</code></pre></div> | |||
<p>Now, when someone reloads or revists a search page, a new search will automatically run.</p> |
@@ -0,0 +1,332 @@ | |||
<!doctype html><!-- This is a valid HTML5 document. --> | |||
<!-- Screen readers, SEO, extensions and so on. --> | |||
<html lang="fr"> | |||
<!-- Has to be within the first 1024 bytes, hence before the `title` element | |||
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset --> | |||
<meta charset="utf-8"> | |||
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 --> | |||
<!-- The viewport meta is quite crowded and we are responsible for that. | |||
See: https://codepen.io/tigt/post/meta-viewport-for-2015 --> | |||
<meta name="viewport" content="width=device-width,initial-scale=1"> | |||
<!-- Required to make a valid HTML5 document. --> | |||
<title>Ajout d’un module de recherche pour Hugo (archive) — David Larlet</title> | |||
<meta name="description" content="Publication mise en cache pour en conserver une trace."> | |||
<!-- That good ol' feed, subscribe :). --> | |||
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/"> | |||
<!-- Generated from https://realfavicongenerator.net/ such a mess. --> | |||
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png"> | |||
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png"> | |||
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png"> | |||
<link rel="manifest" href="/static/david/icons2/site.webmanifest"> | |||
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c"> | |||
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico"> | |||
<meta name="msapplication-TileColor" content="#f7f7f7"> | |||
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml"> | |||
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)"> | |||
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)"> | |||
<!-- Documented, feel free to shoot an email. --> | |||
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css"> | |||
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. --> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<script> | |||
function toggleTheme(themeName) { | |||
document.documentElement.classList.toggle( | |||
'forced-dark', | |||
themeName === 'dark' | |||
) | |||
document.documentElement.classList.toggle( | |||
'forced-light', | |||
themeName === 'light' | |||
) | |||
} | |||
const selectedTheme = localStorage.getItem('theme') | |||
if (selectedTheme !== 'undefined') { | |||
toggleTheme(selectedTheme) | |||
} | |||
</script> | |||
<meta name="robots" content="noindex, nofollow"> | |||
<meta content="origin-when-cross-origin" name="referrer"> | |||
<!-- Canonical URL for SEO purposes --> | |||
<link rel="canonical" href="https://lord.re/posts/206-recherche-pour-un-blog-statique/"> | |||
<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all"> | |||
<article> | |||
<header> | |||
<h1>Ajout d’un module de recherche pour Hugo</h1> | |||
</header> | |||
<nav> | |||
<p class="center"> | |||
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use> | |||
</svg> Accueil</a> • | |||
<a href="https://lord.re/posts/206-recherche-pour-un-blog-statique/" title="Lien vers le contenu original">Source originale</a> | |||
</p> | |||
</nav> | |||
<hr> | |||
<p>Régulièrement j'ai des gens qui ne retrouvent pas un article que j'ai écrit. | |||
Et moi le premier, je cherche souvent pour savoir où j'ai bien pu parler d'un truc. | |||
Et c'est vrai qu'étant donné qu'il y a de plus en plus de contenu sur mon blog, c'est forcément c'est de plus en plus complexe de se rappeler ou de retrouver une page précise.</p> | |||
<p>Étant donné que j'ai déjà ouvert toutes les pages dans mon navigateur, je me base sur l'autocomplétion du navigateur mais c'est loin d'etre parfait.</p> | |||
<p>Jusqu'à présent je recommandai du coup de se rendre sur la <a href="https://lord.re/mono/">monopage</a>. | |||
Cette page contient absolument tout le contenu du site, il suffit donc d'attendre qu'elle charge puis utiliser la fonction recherche du navigateur (<kbd>Ctrl-f</kbd>) mais c'est un usage qui est au final marginal. | |||
Ça fonctionne mais c'est peu plaisant et puis la page est de plus en plus longue à charger vu qu'elle pèse désormais près de 3Mo de HTML (bon une fois gzippé ça tombe à 800Ko).</p> | |||
<p>L'autre technique est tout simplement de chercher via un moteur de recherche conventionnel comme le fameux <strong>DuckDuckGo</strong> ou autre moteur alternatif. | |||
Mon site est pas mal crawlé et donc plutôt bien indexé donc ça fonctionne pas trop mal. | |||
Mais c'est quand même dommage de dépendre de la bonne volonté des moteurs de recherche pour une fonctionnalité que j'aimerai offrir moi-même dans ma quête d'autonomie et d'indépendance.</p> | |||
<h2 id="cahier-des-charges">Cahier des charges</h2> | |||
<p>Donc je voulais un truc adapté à mes contraintes.</p> | |||
<ul> | |||
<li> | |||
<p>La première c'est d'être <strong>statique</strong>, c'est-à-dire que je n'ai rien à installer sur le serveur hébergeant le site. | |||
Pas de PHP, pas de node, pas …</p> | |||
</li> | |||
<li> | |||
<p>La seconde c'est que ce soit du <strong>logiciel Libre</strong> simple à utiliser. | |||
Encore que la simplicité est très négociable si ça fait bien le taf. | |||
Le but étant de ne pas avoir à installer cinquante trucs pour que ça fonctionne.</p> | |||
</li> | |||
<li> | |||
<p>La troisième c'est que pour l'utilisateur ça soit <strong>pratique</strong> donc si c'est plus complexe ou moins performant que de fouiller manuellement la monopage ça sert à rien.</p> | |||
</li> | |||
</ul> | |||
<h2 id="solution-sélectionnée">Solution sélectionnée</h2> | |||
<p>Il y a trois semaines, lors de ma navigation habituelle sur Hacker News je tombe sur un ptit projet qui semble fait pour me titiller : <strong><a href="https://github.com/tinysearch/tinysearch">Tinysearch</a></strong></p> | |||
<p><em>C'est codé en rust, nécessite un bout de javascript + un bout de webassembly et c'est tout.</em> | |||
Il est prévu pour être mis en place dans mon cas d'usage (les sites statiques) donc je n'aurai pas besoin de tordre un outil pour transformer une pince coupante en pince croco…</p> | |||
<p>Après je n'ai pas cherché plus, je suis tombé par hasard sur ça et ça matchait vraiment bien. | |||
Si ça se trouve il existe mieux ailleurs mais pour le moment je saurai m'en contenter.</p> | |||
<h2 id="donc-la-marche-à-suivre-">Donc la marche à suivre ?</h2> | |||
<p>Alors d'un point de vue utilisateur c'est simplement un petit bout de javascript, qui va lancer un bout de webassembly qui contient le moteur de recherche + son index.</p> | |||
<p>Il faut donc au préalable (après la génération du site) créer (tout du moins le remettre à jour) cet index.</p> | |||
<p>Il faut également intégrer dans les pages (ou dans une seule comme j'ai préféré) ajouter un peu de html+js pour charger ça et voilà. | |||
C'est donc vraiment pas intrusif pour le site.</p> | |||
<h2 id="création-de-lindex">Création de l'index</h2> | |||
<p>Alors ce cher <strong>Tinysearch</strong> a besoin d'un index mais ce n'est pas lui qui va le créer. | |||
Enfin il va se créer son index à lui en touillant tout à sa sauce mais il faut lui fournir les données brutes.</p> | |||
<p>Ici, les données brutes, c'est un fichier json contenant la liste de tous les articles. | |||
Pour chaque article il veut un <strong>titre</strong>, une <strong>url</strong> et le <strong>contenu</strong>.</p> | |||
<p>Il faut donc demander à notre cher <strong>Hugo</strong> de nous générer un fichier json avec la bonne syntaxe. | |||
Bon alors sur le <a href="https://github.com/tinysearch/tinysearch">github de Tinysearch</a> on trouve un peu de doc dont une toute fraîche détaillant la marche à suivre pour Hugo mais je l'ai quelque peu modifiée (<del>je la proposerai ptet en retour si j'ai pas de mauvais retours</del> c'est fait).</p> | |||
<p>On va donc devoir créer 1 fichier définissant la structure du fichier.</p> | |||
<details><summary>layout/_default/list.json.json (oui oui, deux fois .json)</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-golang" data-lang="golang"><span><span>[ | |||
</span></span><span><span>{{ <span>range</span> <span>$</span><span>index</span> , <span>$</span><span>e</span> <span>:=</span> .<span>Site</span>.<span>RegularPages</span> }}{{ <span>if</span> <span>$</span><span>index</span> }}, {{<span>end</span>}}{{ <span>dict</span> <span>"title"</span> .<span>Title</span> <span>"url"</span> .<span>Permalink</span> <span>"body"</span> .<span>Plain</span> | <span>jsonify</span> }}{{<span>end</span>}} | |||
</span></span><span><span>]</span></span></code></pre></div> | |||
</details> | |||
<p>Il faut maintenant dire à Hugo de générer ce fichier pour la home uniquement, on a pas besoin de générer un json pour chacune des pages/liste.</p> | |||
<p>Il faut donc éditer la config globale :</p> | |||
<details><summary>extrait du config.toml</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-toml" data-lang="toml"><span><span>[<span>outputs</span>] | |||
</span></span><span><span> <span>home</span> = [<span>"html"</span>,<span>"rss"</span>,<span>"json"</span>]</span></span></code></pre></div> | |||
</details> | |||
<p>Donc là, pour la home, il générera le html habituel, le rss associé mais également le json.</p> | |||
<p>Voilà maintenant à la racine de votre site ouaib vous trouverez votre fichier index.json</p> | |||
<details><summary>extrait du index.json généré</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-json" data-lang="json"><span><span>[ | |||
</span></span><span><span> { | |||
</span></span><span><span> <span>"body"</span>: <span>"blabla"</span>, | |||
</span></span><span><span> <span>"title"</span>: <span>"Recherche"</span>, | |||
</span></span><span><span> <span>"url"</span>: <span>"https://lord.re/recherche/"</span> | |||
</span></span><span><span> }, | |||
</span></span><span><span> { | |||
</span></span><span><span> <span>"body"</span>: <span>"encore du blabla"</span>, | |||
</span></span><span><span> <span>"title"</span>: <span>"Event Horizon"</span>, | |||
</span></span><span><span> <span>"url"</span>: <span>"https://lord.re/visionnages/event-horizon/"</span> | |||
</span></span><span><span> } | |||
</span></span><span><span>]</span></span></code></pre></div> | |||
</details> | |||
<p>Voilà pour la partie création d'index. | |||
Vous remarquerez que ce fichier json peut vite être assez gros. | |||
Dans mon cas, il pèse 1.9Mo (710Ko gzippé) à comparer à la monopage qui fait 2.9Mo (835Ko gzippée).</p> | |||
<h2 id="utilisation-de-tinysearch">Utilisation de Tinysearch</h2> | |||
<p>Bon je zappe la partie installation : c'est un logiciel très jeune en rust qui n'est probablement dans aucune distribution linux pour le moment. | |||
Je l'ai direct git cloné depuis github et j'ai même touillé une variable dans le code pour le faire fonctionner mais ça devrait être amélioré très bientôt cette partie-là.</p> | |||
<p>Bref, la seule chose à faire est de lui donner à manger votre <em>index.json</em>. | |||
Mettez-vous dans un dossier vierge créé pour l'occasion car il va pondre quelques fichiers.</p> | |||
<p><kbd>tinysearch /tmp/www/public/fr/index.json</kbd>. | |||
Et là ça va voltiger dans tous les sens. | |||
Pour le moment, à chaque fois que vous l'invoquerez il va récupérer des paquets rust, compiler quelques morceaux, lire votre index compiler un binaire en rust, le transpiler en webassembly et vous générer donc un fichier <em>js</em>, un <em>wasm</em> (contenant entre autre l'index dans son format à lui) quelques fichiers en plus qui ne seront pas utiles (sauf peut-être le demo.html pour tester vite fait).</p> | |||
<p>Donc vous pouvez y récupérer le fichier js et wasm et le coller dans votre Hugo où bon vous semble, j'ai choisi de foutre ça dans <em>static/js/</em> alors que bon le fichier wasm en vrai n'est pas statique car il changera à chaque nouvelle génération d'index.</p> | |||
<p>Ces deux fichiers peuvent être gzippés (et c'est très fortement recommandé). | |||
Si votre site est assez conséquent le fichier wasm peut rapidement devenir un peu gros mais il se compresse vraiment très bien. | |||
Pour info, mon fichier <em>tinysearch_engine_bg.wasm</em> pèose 2Mo (mais 330Ko gzippé).</p> | |||
<h2 id="création-dune-page-de-recherche">Création d'une page de recherche</h2> | |||
<p>J'aurai pu foutre la recherche direct dans la sidebar mais ça aurait alourdi toutes les pages du site alors que la recherche ne sera pas utilisée à tous les coups.</p> | |||
<p>J'ai donc créé une page grâce à <kbd>hugo new recherche.md</kbd> et à l'intérieur les trois quarts sont du html.</p> | |||
<details><summary>extrait de content/recherche.md</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-html" data-lang="html"><span><span><<span>section</span> <span>class</span><span>=</span><span>"ideas"</span>> | |||
</span></span><span><span><<span>article</span>> | |||
</span></span><span><span>Vous êtes tout tristouille en train de chercher une page en particulier dans mon ptit bordel ? | |||
</span></span><span><span> | |||
</span></span><span><span>Allez on va tenter de la trouver ensemble ! | |||
</span></span><span><span> | |||
</span></span><span><span> <<span>script</span> <span>type</span><span>=</span><span>"module"</span>> | |||
</span></span><span><span> <span>import</span> { <span>search</span>, <span>default</span> <span>as</span> <span>init</span> } <span>from</span> <span>'https://lord.re/js/tinysearch_engine.js'</span>; | |||
</span></span><span><span> window.<span>search</span> <span>=</span> <span>search</span>; | |||
</span></span><span><span> | |||
</span></span><span><span> <span>async</span> <span>function</span> <span>run</span>() { | |||
</span></span><span><span> <span>await</span> <span>init</span>(<span>'https://lord.re/js/tinysearch_engine_bg.wasm'</span>); | |||
</span></span><span><span> } | |||
</span></span><span><span> | |||
</span></span><span><span> <span>run</span>(); | |||
</span></span><span><span> </<span>script</span>> | |||
</span></span><span><span> | |||
</span></span><span><span> <<span>script</span>> | |||
</span></span><span><span> <span>function</span> <span>doSearch</span>() { | |||
</span></span><span><span> <span>let</span> <span>value</span> <span>=</span> document.<span>getElementById</span>(<span>"recherche"</span>).<span>value</span>; | |||
</span></span><span><span> <span>const</span> <span>arr</span> <span>=</span> <span>search</span>(<span>value</span>, <span>21</span>); | |||
</span></span><span><span> <span>let</span> <span>ul</span> <span>=</span> document.<span>getElementById</span>(<span>"results"</span>); | |||
</span></span><span><span> <span>ul</span>.<span>innerHTML</span> <span>=</span> <span>""</span>; | |||
</span></span><span><span> | |||
</span></span><span><span> <span>for</span> (<span>i</span> <span>=</span> <span>0</span>; <span>i</span> <span><</span> <span>arr</span>.<span>length</span>; <span>i</span><span>++</span>) { | |||
</span></span><span><span> <span>var</span> <span>li</span> <span>=</span> document.<span>createElement</span>(<span>"li"</span>); | |||
</span></span><span><span> | |||
</span></span><span><span> <span>let</span> <span>elem</span> <span>=</span> <span>arr</span>[<span>i</span>]; | |||
</span></span><span><span> <span>let</span> <span>elemlink</span> <span>=</span> document.<span>createElement</span>(<span>'a'</span>); | |||
</span></span><span><span> <span>elemlink</span>.<span>innerHTML</span> <span>=</span> <span>elem</span>[<span>0</span>]; | |||
</span></span><span><span> <span>elemlink</span>.<span>setAttribute</span>(<span>'href'</span>, <span>elem</span>[<span>1</span>]); | |||
</span></span><span><span> <span>li</span>.<span>appendChild</span>(<span>elemlink</span>); | |||
</span></span><span><span> | |||
</span></span><span><span> <span>ul</span>.<span>appendChild</span>(<span>li</span>); | |||
</span></span><span><span> } | |||
</span></span><span><span> } | |||
</span></span><span><span> </<span>script</span>> | |||
</span></span><span><span> | |||
</span></span><span><span> <<span>input</span> <span>type</span><span>=</span><span>"text"</span> <span>id</span><span>=</span><span>"recherche"</span> <span>onkeyup</span><span>=</span><span>"doSearch()"</span> <span>style</span><span>=</span><span>"margin:1em;padding:1em;font-size:2rem;background-color:#222;color:#ddd;border-radius:0.3em;border:none;width:90%;box-shadow:inset 0 0 1em #111;"</span> <span>placeholder</span><span>=</span><span>"recherche"</span>> | |||
</span></span><span><span> <<span>ul</span> <span>id</span><span>=</span><span>"results"</span>> | |||
</span></span><span><span> </<span>ul</span>></span></span></code></pre></div> | |||
</details> | |||
<p>Donc on voit qu'il y a le js et le wasm qui sont chargés (faudra que vous adaptiez les url), j'ai un chouilla stylisé la boite d'input et voilà.</p> | |||
<h2 id="profit-">Profit !</h2> | |||
<p>Normailement c'est tout bon.</p> | |||
<p>Enfin normalement si vous avez pas joué les ptits malins avec des <abbr title="Content Security Policy">CSP</abbr>. | |||
Visiblement le webassembly (wasm) nécessite d'avoir <kbd>script-src 'unsafe-eval'</kbd> pour accepter de tourner sinon vous aurez une erreur étrange dans la console.</p> | |||
<p>En gros <strong>Tinysearch</strong> se base sur votre index exhaustif et utilise un <em>bloom filter</em> (j'y connais rien dans ce domaine) ce qui lui permet d'avoir une corresponsdance entre un mot et du contenu. | |||
L'avantage c'est que potentiellement ce nouvel index peut-être vraiment petit par rapport à la taille de données indexées. | |||
L'inconvénient c'est que c'est assez approximatif, certains termes peuvent donner des résultats faux-positifs et aussi quelques faux-négatifs (mais plus rare).</p> | |||
<p>Vu que cet index est transféré au navigateur web et que c'est également le navigateur qui doit s'en dépatouiller lors d'une recherche, on ne peut pas se permettre d'avoir un fichier trop lourdingue. | |||
Du coup c'est un compromis à trouver, pour l'instant c'est pas configurable (tout du moins il faut changer le code et recompiler <strong>Tinysearch</strong>) et c'est fourni avec une valeur ridiculement petite (tout du moins pour le contenu que j'ai).</p> | |||
<p>J'ai passé le <em><abbr title="une valeur qui se trouve dans bin/src/storage.rs à la ligne 68">magic number</abbr></em> de 10 par défaut à 1024. | |||
Le fichier wasm généré est désormais de 2Mo cependant il se gzip à environ 350Ko ce qui est de suite bien plus raisonnable.</p> | |||
<p>Si vous voulez des explications plus en détails sur l'aspect technique de Tinysearch, un ptit tour sur <a href="https://endler.dev/2019/tinysearch/">le blog du créateur du soft</a> où il explique un peu tout. | |||
C'est intéressant mais très technique et en anglais.</p> | |||
<h2 id="pensées-concernant-tinysearch">Pensées concernant Tinysearch</h2> | |||
<p>Le logiciel est <em>vraiment jeune pour le moment</em> et s'oriente d'ailleurs vers une première sortie en version 1. | |||
Du coup ça implique que le code bouge pas mal et que les devs sont vraiment très à l'écoute et réactif.</p> | |||
<p>Il est très probable que son fonctionnement change dans les semaines à venir. | |||
Pour l'instant, on a dépassé le stade du prototype mais on est loin d'un logiciel fini et mature. | |||
Ils sont conscients que le fonctionnement actuel n'est pas optimal.</p> | |||
<p>Il faut pour l'instant modifier le code à la main et recompiler le soft afin de gérer le compromis d'efficacité/poids de l'index.</p> | |||
<p>Ils savent que télécharger et compiler tout un tas de truc lors de son utilisation est pas user-friendly, pas optimisé du tout. | |||
Bref, ce que je raconte aujourd'hui ne sera ptet plus du tout d'actualité d'ici quelque temps.</p> | |||
<h2 id="mettre-à-jour-lindex">Mettre à jour l'index</h2> | |||
<p>Bon maintenant ça veut dire qu'à chaque fois que je rajoute du nouveau contenu je dois désormais recréer l'index. | |||
Ça mériterait d'être placé dans le hook git qui m'automatise la publication du blog, cela dit le logiciel bougeant encore pas mal, je ne vais pas l'optimiser tout de suite.</p> | |||
<h2 id="à-vous-cognacq-jay--à-vous-les-studios-">À vous Cognacq Jay ! À vous les studios !</h2> | |||
</article> | |||
<hr> | |||
<footer> | |||
<p> | |||
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use> | |||
</svg> Accueil</a> • | |||
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use> | |||
</svg> Suivre</a> • | |||
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use> | |||
</svg> Pro</a> • | |||
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use> | |||
</svg> Email</a> • | |||
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use> | |||
</svg> Légal</abbr> | |||
</p> | |||
<template id="theme-selector"> | |||
<form> | |||
<fieldset> | |||
<legend><svg class="icon icon-brightness-contrast"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use> | |||
</svg> Thème</legend> | |||
<label> | |||
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto | |||
</label> | |||
<label> | |||
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé | |||
</label> | |||
<label> | |||
<input type="radio" value="light" name="chosen-color-scheme"> Clair | |||
</label> | |||
</fieldset> | |||
</form> | |||
</template> | |||
</footer> | |||
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script> | |||
<script> | |||
function loadThemeForm(templateName) { | |||
const themeSelectorTemplate = document.querySelector(templateName) | |||
const form = themeSelectorTemplate.content.firstElementChild | |||
themeSelectorTemplate.replaceWith(form) | |||
form.addEventListener('change', (e) => { | |||
const chosenColorScheme = e.target.value | |||
localStorage.setItem('theme', chosenColorScheme) | |||
toggleTheme(chosenColorScheme) | |||
}) | |||
const selectedTheme = localStorage.getItem('theme') | |||
if (selectedTheme && selectedTheme !== 'undefined') { | |||
form.querySelector(`[value="${selectedTheme}"]`).checked = true | |||
} | |||
} | |||
const prefersColorSchemeDark = '(prefers-color-scheme: dark)' | |||
window.addEventListener('load', () => { | |||
let hasDarkRules = false | |||
for (const styleSheet of Array.from(document.styleSheets)) { | |||
let mediaRules = [] | |||
for (const cssRule of styleSheet.cssRules) { | |||
if (cssRule.type !== CSSRule.MEDIA_RULE) { | |||
continue | |||
} | |||
// WARNING: Safari does not have/supports `conditionText`. | |||
if (cssRule.conditionText) { | |||
if (cssRule.conditionText !== prefersColorSchemeDark) { | |||
continue | |||
} | |||
} else { | |||
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) { | |||
continue | |||
} | |||
} | |||
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules)) | |||
} | |||
// WARNING: do not try to insert a Rule to a styleSheet you are | |||
// currently iterating on, otherwise the browser will be stuck | |||
// in a infinite loop… | |||
for (const mediaRule of mediaRules) { | |||
styleSheet.insertRule(mediaRule.cssText) | |||
hasDarkRules = true | |||
} | |||
} | |||
if (hasDarkRules) { | |||
loadThemeForm('#theme-selector') | |||
} | |||
}) | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,165 @@ | |||
title: Ajout d’un module de recherche pour Hugo | |||
url: https://lord.re/posts/206-recherche-pour-un-blog-statique/ | |||
hash_url: 2f3ed5cb927427fb834b4a9d657592be | |||
<p>Régulièrement j'ai des gens qui ne retrouvent pas un article que j'ai écrit. | |||
Et moi le premier, je cherche souvent pour savoir où j'ai bien pu parler d'un truc. | |||
Et c'est vrai qu'étant donné qu'il y a de plus en plus de contenu sur mon blog, c'est forcément c'est de plus en plus complexe de se rappeler ou de retrouver une page précise.</p> | |||
<p>Étant donné que j'ai déjà ouvert toutes les pages dans mon navigateur, je me base sur l'autocomplétion du navigateur mais c'est loin d'etre parfait.</p> | |||
<p>Jusqu'à présent je recommandai du coup de se rendre sur la <a href="https://lord.re/mono/">monopage</a>. | |||
Cette page contient absolument tout le contenu du site, il suffit donc d'attendre qu'elle charge puis utiliser la fonction recherche du navigateur (<kbd>Ctrl-f</kbd>) mais c'est un usage qui est au final marginal. | |||
Ça fonctionne mais c'est peu plaisant et puis la page est de plus en plus longue à charger vu qu'elle pèse désormais près de 3Mo de HTML (bon une fois gzippé ça tombe à 800Ko).</p> | |||
<p>L'autre technique est tout simplement de chercher via un moteur de recherche conventionnel comme le fameux <strong>DuckDuckGo</strong> ou autre moteur alternatif. | |||
Mon site est pas mal crawlé et donc plutôt bien indexé donc ça fonctionne pas trop mal. | |||
Mais c'est quand même dommage de dépendre de la bonne volonté des moteurs de recherche pour une fonctionnalité que j'aimerai offrir moi-même dans ma quête d'autonomie et d'indépendance.</p> | |||
<h2 id="cahier-des-charges">Cahier des charges</h2> | |||
<p>Donc je voulais un truc adapté à mes contraintes.</p> | |||
<ul> | |||
<li> | |||
<p>La première c'est d'être <strong>statique</strong>, c'est-à-dire que je n'ai rien à installer sur le serveur hébergeant le site. | |||
Pas de PHP, pas de node, pas …</p> | |||
</li> | |||
<li> | |||
<p>La seconde c'est que ce soit du <strong>logiciel Libre</strong> simple à utiliser. | |||
Encore que la simplicité est très négociable si ça fait bien le taf. | |||
Le but étant de ne pas avoir à installer cinquante trucs pour que ça fonctionne.</p> | |||
</li> | |||
<li> | |||
<p>La troisième c'est que pour l'utilisateur ça soit <strong>pratique</strong> donc si c'est plus complexe ou moins performant que de fouiller manuellement la monopage ça sert à rien.</p> | |||
</li> | |||
</ul> | |||
<h2 id="solution-sélectionnée">Solution sélectionnée</h2> | |||
<p>Il y a trois semaines, lors de ma navigation habituelle sur Hacker News je tombe sur un ptit projet qui semble fait pour me titiller : <strong><a href="https://github.com/tinysearch/tinysearch">Tinysearch</a></strong></p> | |||
<p><em>C'est codé en rust, nécessite un bout de javascript + un bout de webassembly et c'est tout.</em> | |||
Il est prévu pour être mis en place dans mon cas d'usage (les sites statiques) donc je n'aurai pas besoin de tordre un outil pour transformer une pince coupante en pince croco…</p> | |||
<p>Après je n'ai pas cherché plus, je suis tombé par hasard sur ça et ça matchait vraiment bien. | |||
Si ça se trouve il existe mieux ailleurs mais pour le moment je saurai m'en contenter.</p> | |||
<h2 id="donc-la-marche-à-suivre-">Donc la marche à suivre ?</h2> | |||
<p>Alors d'un point de vue utilisateur c'est simplement un petit bout de javascript, qui va lancer un bout de webassembly qui contient le moteur de recherche + son index.</p> | |||
<p>Il faut donc au préalable (après la génération du site) créer (tout du moins le remettre à jour) cet index.</p> | |||
<p>Il faut également intégrer dans les pages (ou dans une seule comme j'ai préféré) ajouter un peu de html+js pour charger ça et voilà. | |||
C'est donc vraiment pas intrusif pour le site.</p> | |||
<h2 id="création-de-lindex">Création de l'index</h2> | |||
<p>Alors ce cher <strong>Tinysearch</strong> a besoin d'un index mais ce n'est pas lui qui va le créer. | |||
Enfin il va se créer son index à lui en touillant tout à sa sauce mais il faut lui fournir les données brutes.</p> | |||
<p>Ici, les données brutes, c'est un fichier json contenant la liste de tous les articles. | |||
Pour chaque article il veut un <strong>titre</strong>, une <strong>url</strong> et le <strong>contenu</strong>.</p> | |||
<p>Il faut donc demander à notre cher <strong>Hugo</strong> de nous générer un fichier json avec la bonne syntaxe. | |||
Bon alors sur le <a href="https://github.com/tinysearch/tinysearch">github de Tinysearch</a> on trouve un peu de doc dont une toute fraîche détaillant la marche à suivre pour Hugo mais je l'ai quelque peu modifiée (<del>je la proposerai ptet en retour si j'ai pas de mauvais retours</del> c'est fait).</p> | |||
<p>On va donc devoir créer 1 fichier définissant la structure du fichier.</p> | |||
<details><summary>layout/_default/list.json.json (oui oui, deux fois .json)</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-golang" data-lang="golang"><span><span>[ | |||
</span></span><span><span>{{ <span>range</span> <span>$</span><span>index</span> , <span>$</span><span>e</span> <span>:=</span> .<span>Site</span>.<span>RegularPages</span> }}{{ <span>if</span> <span>$</span><span>index</span> }}, {{<span>end</span>}}{{ <span>dict</span> <span>"title"</span> .<span>Title</span> <span>"url"</span> .<span>Permalink</span> <span>"body"</span> .<span>Plain</span> | <span>jsonify</span> }}{{<span>end</span>}} | |||
</span></span><span><span>]</span></span></code></pre></div> | |||
</details> | |||
<p>Il faut maintenant dire à Hugo de générer ce fichier pour la home uniquement, on a pas besoin de générer un json pour chacune des pages/liste.</p> | |||
<p>Il faut donc éditer la config globale :</p> | |||
<details><summary>extrait du config.toml</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-toml" data-lang="toml"><span><span>[<span>outputs</span>] | |||
</span></span><span><span> <span>home</span> = [<span>"html"</span>,<span>"rss"</span>,<span>"json"</span>]</span></span></code></pre></div> | |||
</details> | |||
<p>Donc là, pour la home, il générera le html habituel, le rss associé mais également le json.</p> | |||
<p>Voilà maintenant à la racine de votre site ouaib vous trouverez votre fichier index.json</p> | |||
<details><summary>extrait du index.json généré</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-json" data-lang="json"><span><span>[ | |||
</span></span><span><span> { | |||
</span></span><span><span> <span>"body"</span>: <span>"blabla"</span>, | |||
</span></span><span><span> <span>"title"</span>: <span>"Recherche"</span>, | |||
</span></span><span><span> <span>"url"</span>: <span>"https://lord.re/recherche/"</span> | |||
</span></span><span><span> }, | |||
</span></span><span><span> { | |||
</span></span><span><span> <span>"body"</span>: <span>"encore du blabla"</span>, | |||
</span></span><span><span> <span>"title"</span>: <span>"Event Horizon"</span>, | |||
</span></span><span><span> <span>"url"</span>: <span>"https://lord.re/visionnages/event-horizon/"</span> | |||
</span></span><span><span> } | |||
</span></span><span><span>]</span></span></code></pre></div> | |||
</details> | |||
<p>Voilà pour la partie création d'index. | |||
Vous remarquerez que ce fichier json peut vite être assez gros. | |||
Dans mon cas, il pèse 1.9Mo (710Ko gzippé) à comparer à la monopage qui fait 2.9Mo (835Ko gzippée).</p> | |||
<h2 id="utilisation-de-tinysearch">Utilisation de Tinysearch</h2> | |||
<p>Bon je zappe la partie installation : c'est un logiciel très jeune en rust qui n'est probablement dans aucune distribution linux pour le moment. | |||
Je l'ai direct git cloné depuis github et j'ai même touillé une variable dans le code pour le faire fonctionner mais ça devrait être amélioré très bientôt cette partie-là.</p> | |||
<p>Bref, la seule chose à faire est de lui donner à manger votre <em>index.json</em>. | |||
Mettez-vous dans un dossier vierge créé pour l'occasion car il va pondre quelques fichiers.</p> | |||
<p><kbd>tinysearch /tmp/www/public/fr/index.json</kbd>. | |||
Et là ça va voltiger dans tous les sens. | |||
Pour le moment, à chaque fois que vous l'invoquerez il va récupérer des paquets rust, compiler quelques morceaux, lire votre index compiler un binaire en rust, le transpiler en webassembly et vous générer donc un fichier <em>js</em>, un <em>wasm</em> (contenant entre autre l'index dans son format à lui) quelques fichiers en plus qui ne seront pas utiles (sauf peut-être le demo.html pour tester vite fait).</p> | |||
<p>Donc vous pouvez y récupérer le fichier js et wasm et le coller dans votre Hugo où bon vous semble, j'ai choisi de foutre ça dans <em>static/js/</em> alors que bon le fichier wasm en vrai n'est pas statique car il changera à chaque nouvelle génération d'index.</p> | |||
<p>Ces deux fichiers peuvent être gzippés (et c'est très fortement recommandé). | |||
Si votre site est assez conséquent le fichier wasm peut rapidement devenir un peu gros mais il se compresse vraiment très bien. | |||
Pour info, mon fichier <em>tinysearch_engine_bg.wasm</em> pèose 2Mo (mais 330Ko gzippé).</p> | |||
<h2 id="création-dune-page-de-recherche">Création d'une page de recherche</h2> | |||
<p>J'aurai pu foutre la recherche direct dans la sidebar mais ça aurait alourdi toutes les pages du site alors que la recherche ne sera pas utilisée à tous les coups.</p> | |||
<p>J'ai donc créé une page grâce à <kbd>hugo new recherche.md</kbd> et à l'intérieur les trois quarts sont du html.</p> | |||
<details><summary>extrait de content/recherche.md</summary> | |||
<div class="highlight"><pre tabindex="0"><code class="language-html" data-lang="html"><span><span><<span>section</span> <span>class</span><span>=</span><span>"ideas"</span>> | |||
</span></span><span><span><<span>article</span>> | |||
</span></span><span><span>Vous êtes tout tristouille en train de chercher une page en particulier dans mon ptit bordel ? | |||
</span></span><span><span> | |||
</span></span><span><span>Allez on va tenter de la trouver ensemble ! | |||
</span></span><span><span> | |||
</span></span><span><span> <<span>script</span> <span>type</span><span>=</span><span>"module"</span>> | |||
</span></span><span><span> <span>import</span> { <span>search</span>, <span>default</span> <span>as</span> <span>init</span> } <span>from</span> <span>'https://lord.re/js/tinysearch_engine.js'</span>; | |||
</span></span><span><span> window.<span>search</span> <span>=</span> <span>search</span>; | |||
</span></span><span><span> | |||
</span></span><span><span> <span>async</span> <span>function</span> <span>run</span>() { | |||
</span></span><span><span> <span>await</span> <span>init</span>(<span>'https://lord.re/js/tinysearch_engine_bg.wasm'</span>); | |||
</span></span><span><span> } | |||
</span></span><span><span> | |||
</span></span><span><span> <span>run</span>(); | |||
</span></span><span><span> </<span>script</span>> | |||
</span></span><span><span> | |||
</span></span><span><span> <<span>script</span>> | |||
</span></span><span><span> <span>function</span> <span>doSearch</span>() { | |||
</span></span><span><span> <span>let</span> <span>value</span> <span>=</span> document.<span>getElementById</span>(<span>"recherche"</span>).<span>value</span>; | |||
</span></span><span><span> <span>const</span> <span>arr</span> <span>=</span> <span>search</span>(<span>value</span>, <span>21</span>); | |||
</span></span><span><span> <span>let</span> <span>ul</span> <span>=</span> document.<span>getElementById</span>(<span>"results"</span>); | |||
</span></span><span><span> <span>ul</span>.<span>innerHTML</span> <span>=</span> <span>""</span>; | |||
</span></span><span><span> | |||
</span></span><span><span> <span>for</span> (<span>i</span> <span>=</span> <span>0</span>; <span>i</span> <span><</span> <span>arr</span>.<span>length</span>; <span>i</span><span>++</span>) { | |||
</span></span><span><span> <span>var</span> <span>li</span> <span>=</span> document.<span>createElement</span>(<span>"li"</span>); | |||
</span></span><span><span> | |||
</span></span><span><span> <span>let</span> <span>elem</span> <span>=</span> <span>arr</span>[<span>i</span>]; | |||
</span></span><span><span> <span>let</span> <span>elemlink</span> <span>=</span> document.<span>createElement</span>(<span>'a'</span>); | |||
</span></span><span><span> <span>elemlink</span>.<span>innerHTML</span> <span>=</span> <span>elem</span>[<span>0</span>]; | |||
</span></span><span><span> <span>elemlink</span>.<span>setAttribute</span>(<span>'href'</span>, <span>elem</span>[<span>1</span>]); | |||
</span></span><span><span> <span>li</span>.<span>appendChild</span>(<span>elemlink</span>); | |||
</span></span><span><span> | |||
</span></span><span><span> <span>ul</span>.<span>appendChild</span>(<span>li</span>); | |||
</span></span><span><span> } | |||
</span></span><span><span> } | |||
</span></span><span><span> </<span>script</span>> | |||
</span></span><span><span> | |||
</span></span><span><span> <<span>input</span> <span>type</span><span>=</span><span>"text"</span> <span>id</span><span>=</span><span>"recherche"</span> <span>onkeyup</span><span>=</span><span>"doSearch()"</span> <span>style</span><span>=</span><span>"margin:1em;padding:1em;font-size:2rem;background-color:#222;color:#ddd;border-radius:0.3em;border:none;width:90%;box-shadow:inset 0 0 1em #111;"</span> <span>placeholder</span><span>=</span><span>"recherche"</span>> | |||
</span></span><span><span> <<span>ul</span> <span>id</span><span>=</span><span>"results"</span>> | |||
</span></span><span><span> </<span>ul</span>></span></span></code></pre></div> | |||
</details> | |||
<p>Donc on voit qu'il y a le js et le wasm qui sont chargés (faudra que vous adaptiez les url), j'ai un chouilla stylisé la boite d'input et voilà.</p> | |||
<h2 id="profit-">Profit !</h2> | |||
<p>Normailement c'est tout bon.</p> | |||
<p>Enfin normalement si vous avez pas joué les ptits malins avec des <abbr title="Content Security Policy">CSP</abbr>. | |||
Visiblement le webassembly (wasm) nécessite d'avoir <kbd>script-src 'unsafe-eval'</kbd> pour accepter de tourner sinon vous aurez une erreur étrange dans la console.</p> | |||
<p>En gros <strong>Tinysearch</strong> se base sur votre index exhaustif et utilise un <em>bloom filter</em> (j'y connais rien dans ce domaine) ce qui lui permet d'avoir une corresponsdance entre un mot et du contenu. | |||
L'avantage c'est que potentiellement ce nouvel index peut-être vraiment petit par rapport à la taille de données indexées. | |||
L'inconvénient c'est que c'est assez approximatif, certains termes peuvent donner des résultats faux-positifs et aussi quelques faux-négatifs (mais plus rare).</p> | |||
<p>Vu que cet index est transféré au navigateur web et que c'est également le navigateur qui doit s'en dépatouiller lors d'une recherche, on ne peut pas se permettre d'avoir un fichier trop lourdingue. | |||
Du coup c'est un compromis à trouver, pour l'instant c'est pas configurable (tout du moins il faut changer le code et recompiler <strong>Tinysearch</strong>) et c'est fourni avec une valeur ridiculement petite (tout du moins pour le contenu que j'ai).</p> | |||
<p>J'ai passé le <em><abbr title="une valeur qui se trouve dans bin/src/storage.rs à la ligne 68">magic number</abbr></em> de 10 par défaut à 1024. | |||
Le fichier wasm généré est désormais de 2Mo cependant il se gzip à environ 350Ko ce qui est de suite bien plus raisonnable.</p> | |||
<p>Si vous voulez des explications plus en détails sur l'aspect technique de Tinysearch, un ptit tour sur <a href="https://endler.dev/2019/tinysearch/">le blog du créateur du soft</a> où il explique un peu tout. | |||
C'est intéressant mais très technique et en anglais.</p> | |||
<h2 id="pensées-concernant-tinysearch">Pensées concernant Tinysearch</h2> | |||
<p>Le logiciel est <em>vraiment jeune pour le moment</em> et s'oriente d'ailleurs vers une première sortie en version 1. | |||
Du coup ça implique que le code bouge pas mal et que les devs sont vraiment très à l'écoute et réactif.</p> | |||
<p>Il est très probable que son fonctionnement change dans les semaines à venir. | |||
Pour l'instant, on a dépassé le stade du prototype mais on est loin d'un logiciel fini et mature. | |||
Ils sont conscients que le fonctionnement actuel n'est pas optimal.</p> | |||
<p>Il faut pour l'instant modifier le code à la main et recompiler le soft afin de gérer le compromis d'efficacité/poids de l'index.</p> | |||
<p>Ils savent que télécharger et compiler tout un tas de truc lors de son utilisation est pas user-friendly, pas optimisé du tout. | |||
Bref, ce que je raconte aujourd'hui ne sera ptet plus du tout d'actualité d'ici quelque temps.</p> | |||
<h2 id="mettre-à-jour-lindex">Mettre à jour l'index</h2> | |||
<p>Bon maintenant ça veut dire qu'à chaque fois que je rajoute du nouveau contenu je dois désormais recréer l'index. | |||
Ça mériterait d'être placé dans le hook git qui m'automatise la publication du blog, cela dit le logiciel bougeant encore pas mal, je ne vais pas l'optimiser tout de suite.</p> | |||
<h2 id="à-vous-cognacq-jay--à-vous-les-studios-">À vous Cognacq Jay ! À vous les studios !</h2> |
@@ -0,0 +1,451 @@ | |||
<!doctype html><!-- This is a valid HTML5 document. --> | |||
<!-- Screen readers, SEO, extensions and so on. --> | |||
<html lang="fr"> | |||
<!-- Has to be within the first 1024 bytes, hence before the `title` element | |||
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset --> | |||
<meta charset="utf-8"> | |||
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 --> | |||
<!-- The viewport meta is quite crowded and we are responsible for that. | |||
See: https://codepen.io/tigt/post/meta-viewport-for-2015 --> | |||
<meta name="viewport" content="width=device-width,initial-scale=1"> | |||
<!-- Required to make a valid HTML5 document. --> | |||
<title>Technical Solutions Poorly Solve Social Problems (archive) — David Larlet</title> | |||
<meta name="description" content="Publication mise en cache pour en conserver une trace."> | |||
<!-- That good ol' feed, subscribe :). --> | |||
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/"> | |||
<!-- Generated from https://realfavicongenerator.net/ such a mess. --> | |||
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png"> | |||
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png"> | |||
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png"> | |||
<link rel="manifest" href="/static/david/icons2/site.webmanifest"> | |||
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c"> | |||
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico"> | |||
<meta name="msapplication-TileColor" content="#f7f7f7"> | |||
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml"> | |||
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)"> | |||
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)"> | |||
<!-- Documented, feel free to shoot an email. --> | |||
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css"> | |||
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. --> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin> | |||
<script> | |||
function toggleTheme(themeName) { | |||
document.documentElement.classList.toggle( | |||
'forced-dark', | |||
themeName === 'dark' | |||
) | |||
document.documentElement.classList.toggle( | |||
'forced-light', | |||
themeName === 'light' | |||
) | |||
} | |||
const selectedTheme = localStorage.getItem('theme') | |||
if (selectedTheme !== 'undefined') { | |||
toggleTheme(selectedTheme) | |||
} | |||
</script> | |||
<meta name="robots" content="noindex, nofollow"> | |||
<meta content="origin-when-cross-origin" name="referrer"> | |||
<!-- Canonical URL for SEO purposes --> | |||
<link rel="canonical" href="https://christine.website/blog/social-quandry-devops-2022-03-17"> | |||
<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all"> | |||
<article> | |||
<header> | |||
<h1>Technical Solutions Poorly Solve Social Problems</h1> | |||
</header> | |||
<nav> | |||
<p class="center"> | |||
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home"> | |||
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use> | |||
</svg> Accueil</a> • | |||
<a href="https://christine.website/blog/social-quandry-devops-2022-03-17" title="Lien vers le contenu original">Source originale</a> | |||
</p> | |||
</nav> | |||
<hr> | |||
<p class="conversation-chat"><<b>Cadey</b>> I just wanna lead this article out by saying that <em>I do not have all the | |||
answers here</em>. I really wish I did, but I also feel that I shouldn't have to | |||
have an answer in mind in order to raise a question. Please also keep in mind | |||
that this is coming from someone who has been working in devops for most of | |||
their career.</p> | |||
</div> | |||
<h2>Or: The Social Quandry of Devops</h2> | |||
<p>Technology is the cornerstone of our society. As a people we have seen the | |||
catalytic things that technology has enabled us to do. Through technology and | |||
new and innovative ways of applying it, we can help solve many problems. This | |||
leads some to envision technology as a panacea, a mythical cure-all that will | |||
make all our problems go away with the right use of it.</p> | |||
<p>This does not extend to social problems. Technical fixes for social problems are | |||
how we end up with an inadequate mess that can make the problem a lot worse than | |||
it was before. You've almost certainly been able to see this in action with | |||
social media (under the belief that allowing people to connect is so morally | |||
correct that it will bring in a new age of humanity that will be objectively | |||
good for everyone). The example I want to focus on today is the Devops | |||
philosophy. Devops is a technical solution (creating a new department) that | |||
helps work around social problems in workplaces (fundamental differences in | |||
priorities and end goals), and in the process it doesn't solve either very well.</p> | |||
<p>There are a lot of skillset paths that you can end up with in tech, but the two | |||
biggest ones are development (making the computer do new things) and systems | |||
administration (making computers keep doing those things). There are many other | |||
silos in the industry (technical writing, project/product management, etc.), but | |||
the two main ones are development and systems administration. These two groups | |||
have vastly different priorities, skillsets, needs and future goals, and as a | |||
result of this there is very little natural cross-pollenation between the two | |||
silos. I have seen this evolve into cultural resentment.</p> | |||
<div class="conversation"> | |||
<p class="conversation-chat"><<b>Cadey</b>> Not to say that this phenomenon is exclusive to inter-department ties, I've | |||
also seen it happen intra-department over choice of programming language.</p> | |||
</div> | |||
<p>As far as the main differences go, development usually sees what could be. What | |||
new things could exist and what steps you need to take to get people there. This | |||
usually involves designing and implementing new software. The systems | |||
administration side of things is more likely to see it as a matter of | |||
integrating things into an existing whole, and then ensuring that whole is | |||
reliable and proven so they don't have to worry about it constantly. This causes | |||
a slower velocity forward and can result in extra process, slow momentum and | |||
stagnation. These two forces naturally come into conflict because they are | |||
vastly different things and have vastly different requirements and expectations.</p> | |||
<p>Development may want to use a new version of the compiler to support a language | |||
feature that will eliminate a lot of repetitive boilerplate. The sysadmins may | |||
not be able to ship that compiler in production build toolstack because of | |||
conflicting dependencies elsewhere, but they may also not want to ship that | |||
compiler because of fears over trusting unproven software in production.</p> | |||
<div class="conversation"> | |||
<p class="conversation-chat"><<b>Cadey</b>> This fear sounds really odd at first glance, but this is a paraphrased version | |||
of a problem I actually encountered in the real world at one of my first big | |||
tech jobs. This place had some unique tech choices such as making their own fork | |||
of Ubuntu for "stability reasons", and the process to upgrade tools was a huge | |||
pain on the sysadmin side because it meant retesting and deploying a lot of | |||
internal tooling, which took a lot longer than the engineering team had patience | |||
for. This may not be the best example from a technical standpoint, but things | |||
don't have to make sense for them to exist.</p> | |||
</div> | |||
<p>This tension builds over a long period of time and can cause problems when the | |||
sysadmin team is chronically underfunded (due to the idea that they are | |||
successful when nothing goes wrong, also incurring the problem of success being | |||
a negative, which can make the sysadmin team look like a money pit when they are | |||
actually the very thing that is making the money generator generate money). This | |||
can also lead to avoidable burnout, unwarranted anxiety issues and unneeded | |||
suffering on both ends of the conflict.</p> | |||
<p>So given the unstoppable force of development and the immovable wall of | |||
sysadmin, an organizational compromise was made. This started out as many things | |||
with many names, but as the idea rippled throughout people's heads the name | |||
"devops" ended up sticking. Devops is a hybrid of traditional software | |||