Browse Source

More links

master
David Larlet 2 years ago
parent
commit
b3508cd2d3
25 changed files with 4491 additions and 0 deletions
  1. 466
    0
      cache/2022/09da2c716e8bb15e5d22ee071cf39358/index.html
  2. 299
    0
      cache/2022/09da2c716e8bb15e5d22ee071cf39358/index.md
  3. 275
    0
      cache/2022/2c67b87e1b880952bb277fc429cb8bf5/index.html
  4. 108
    0
      cache/2022/2c67b87e1b880952bb277fc429cb8bf5/index.md
  5. 332
    0
      cache/2022/2f3ed5cb927427fb834b4a9d657592be/index.html
  6. 165
    0
      cache/2022/2f3ed5cb927427fb834b4a9d657592be/index.md
  7. 451
    0
      cache/2022/3e8bb1b63246d6f97316864569492382/index.html
  8. 294
    0
      cache/2022/3e8bb1b63246d6f97316864569492382/index.md
  9. 188
    0
      cache/2022/4bf828ef0ce7191d048d0c510a3c3e0c/index.html
  10. 21
    0
      cache/2022/4bf828ef0ce7191d048d0c510a3c3e0c/index.md
  11. 191
    0
      cache/2022/5eb0016b355ac4b358be367fe64f4c84/index.html
  12. 24
    0
      cache/2022/5eb0016b355ac4b358be367fe64f4c84/index.md
  13. 236
    0
      cache/2022/a863c20d0cb9722df74219009e8365a3/index.html
  14. 13
    0
      cache/2022/a863c20d0cb9722df74219009e8365a3/index.md
  15. 201
    0
      cache/2022/c7ebf32ee18c4f44c452f864729a21a8/index.html
  16. 8
    0
      cache/2022/c7ebf32ee18c4f44c452f864729a21a8/index.md
  17. 182
    0
      cache/2022/cf85372fcb8da232d3fb8d95a88bc8fe/index.html
  18. 15
    0
      cache/2022/cf85372fcb8da232d3fb8d95a88bc8fe/index.md
  19. 341
    0
      cache/2022/d97914db7d2e525edc27669adbc0f917/index.html
  20. 78
    0
      cache/2022/d97914db7d2e525edc27669adbc0f917/index.md
  21. 203
    0
      cache/2022/d9af1ba02055491fc25b6849b8fd65d0/index.html
  22. 5
    0
      cache/2022/d9af1ba02055491fc25b6849b8fd65d0/index.md
  23. 269
    0
      cache/2022/ed7544349c2bef8c7f1bfff3ab286fd6/index.html
  24. 102
    0
      cache/2022/ed7544349c2bef8c7f1bfff3ab286fd6/index.md
  25. 24
    0
      cache/2022/index.html

+ 466
- 0
cache/2022/09da2c716e8bb15e5d22ee071cf39358/index.html View File

@@ -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">&lt;</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">&gt;</span>
<span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-search"</span><span class="p">&gt;</span>Enter your search criteria:<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
<span class="p">&lt;</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">&gt;</span>
<span class="p">&lt;</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">&gt;</span>
<span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>Search<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</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">&lt;</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">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-results"</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</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">&amp;&amp;</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">&gt;</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">`&lt;p&gt;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&lt;/p&gt;`</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">`&lt;p&gt;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&lt;/p&gt;`</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">'&lt;p&gt;Sorry, no matches were found.&lt;/p&gt;'</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>

+ 299
- 0
cache/2022/09da2c716e8bb15e5d22ee071cf39358/index.md View File

@@ -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">&lt;</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">&gt;</span>
<span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-search"</span><span class="p">&gt;</span>Enter your search criteria:<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
<span class="p">&lt;</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">&gt;</span>
<span class="p">&lt;</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">&gt;</span>
<span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>Search<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</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">&lt;</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">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-results"</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</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">&amp;&amp;</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">&gt;</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">`&lt;p&gt;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&lt;/p&gt;`</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">`&lt;p&gt;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&lt;/p&gt;`</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">'&lt;p&gt;Sorry, no matches were found.&lt;/p&gt;'</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>

+ 275
- 0
cache/2022/2c67b87e1b880952bb277fc429cb8bf5/index.html View File

@@ -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>

+ 108
- 0
cache/2022/2c67b87e1b880952bb277fc429cb8bf5/index.md View File

@@ -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>

+ 332
- 0
cache/2022/2f3ed5cb927427fb834b4a9d657592be/index.html View File

@@ -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>&lt;<span>section</span> <span>class</span><span>=</span><span>"ideas"</span>&gt;
</span></span><span><span>&lt;<span>article</span>&gt;
</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> &lt;<span>script</span> <span>type</span><span>=</span><span>"module"</span>&gt;
</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> &lt;/<span>script</span>&gt;
</span></span><span><span>
</span></span><span><span> &lt;<span>script</span>&gt;
</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>&lt;</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> &lt;/<span>script</span>&gt;
</span></span><span><span>
</span></span><span><span> &lt;<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>&gt;
</span></span><span><span> &lt;<span>ul</span> <span>id</span><span>=</span><span>"results"</span>&gt;
</span></span><span><span> &lt;/<span>ul</span>&gt;</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>

+ 165
- 0
cache/2022/2f3ed5cb927427fb834b4a9d657592be/index.md View File

@@ -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>&lt;<span>section</span> <span>class</span><span>=</span><span>"ideas"</span>&gt;
</span></span><span><span>&lt;<span>article</span>&gt;
</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> &lt;<span>script</span> <span>type</span><span>=</span><span>"module"</span>&gt;
</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> &lt;/<span>script</span>&gt;
</span></span><span><span>
</span></span><span><span> &lt;<span>script</span>&gt;
</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>&lt;</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> &lt;/<span>script</span>&gt;
</span></span><span><span>
</span></span><span><span> &lt;<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>&gt;
</span></span><span><span> &lt;<span>ul</span> <span>id</span><span>=</span><span>"results"</span>&gt;
</span></span><span><span> &lt;/<span>ul</span>&gt;</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>

+ 451
- 0
cache/2022/3e8bb1b63246d6f97316864569492382/index.html View File

@@ -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">&lt;<b>Cadey</b>&gt; 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">&lt;<b>Cadey</b>&gt; 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">&lt;<b>Cadey</b>&gt; 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
development and systems administration. On paper this should be great. The silos
will shrink. People will understand the limits and needs of the others. Managers
will be able to have more flexible employees.</p>
<p>Unfortunately though, a lot of the ideas behind devops and the overall
philosophy really do require you to radically burn down everything and start
from scratch. This tends to really not be conducive to engineering timetables
and overall system stability during the age of turbulence.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; What's the problem with burning everything down? Fire cleanses all things and
purifies away the unworthy!</p>
</div>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; Not when you're the one being burned!</p>
</div>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; Wait, so what actually happens then? Does it just end up being a sysadmin team
made up out of coders?</p>
</div>

<p>Yeah, in practice this ends up being a "new team" or a reboot of an existing
team in ways that is suddenly compelling or sexy to executives because a new
buzzword is on the scene. Realistically, devops did end up getting a proper
definition at a buzzword conference level (being able to handle development and
deployment of services from editor to production), but in practice this ends up
being just some random developers that you tricked into caring about production
now while also telling them that they're better than the sysadmins.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; Two jobs for the price of one!</p>
</div>

<p>This ends up shafting the sysadmin team even harder because the new fancy devops
team has things they can talk about as positives for their quarters, so people
can more easily make a case for promotion. As a sysadmin, your "success" case is
"bad things didn't happen", which means success can't stand out on reviews.
Consider "scaled production above the rate of our customer acquistion rate"
against "set up continuous delivery to ensure velocity on our team, saving 50
hours of effort per week". Which one of those do you think gets you promoted?
Which one of those do you think gets headcount for new hires?</p>
<p>This has human costs too. At one of my past jobs doing more sysadmin-y things
(it was marketed as a devops hybrid role, but the "hybrid" part was more of
"frantically patch up the sinking ship with code" and not traditional software
development). Sleep is really essential to helping you function properly to do
your job. During the times when I was pager bitch, there was at least a 1/8
chance that I would be woken up in the middle of the night to handle a problem.
I had to change my pager tone 15 times and still get goosebumps hearing those
old sounds nearly a decade later. This ended up being a huge factor in my
developing anxiety issues that I still feel today. I ended up getting addicted
to weed really bad for a few years. I admit that I'm really not the most robust
person in the world, but these things add up.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; I guess "addicted to weed" isn't totally accurate or inaccurate here, it's more
that I was addicted to the feeling of being high rather than dependence on the
drug itself. Either way, it was bad and weed was my cope. It also probably
really didn't help that I was also starting hormone replacement therapy at the
time, so I was going through second puberty at the time as well. This is the
kind of human capital cost when dealing with dysfunction like this. I've always
been kind of afraid to speak up about this.</p>
</div>

<p>However, there are real technical problems that can only really be solved from a
devops perspective. Tools like Docker would probably never have happened in the
way they did if the devops philosophy didn't exist.</p>
<p><img src="https://cdn.christine.website/file/christine-static/blog/1BDBBB94-7052-4E4C-AE32-CFEE4226CBA8.jpeg" alt="A three panel meme with an old man talking to a child. The child says &quot;it works on my machine&quot;. The old man replies with &quot;then we'll ship your machine&quot;. The last panel says &quot;and that is how docker was born&quot;."></p>
<p>In a way, Docker is one of the perfect examples of the devops philosophy. It
allows developers to have their own custom versions of everything. They can use
custom compilers that the sysadmins don't have to integrate into everything.
They can experiment with new toolstacks, languages and build systems without
worrying about how they integrate into existing processes. And in the process it
defaults to things that are so hilariously unsafe that you only really realize
the problems when they own you. It makes it easy to ship around configurations
for services yes, but it doesn't make supply chain management easy at all.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; Wait, what about that? How does that make any sense?</p>
</div>

<p>Okay, let's consider this basic Dockerfile that builds a Go service. If you
start from very little knowledge of what's going on, you'd probably end up with
something like this:</p>
<pre><code class="language-Dockerfile">
<span>FROM golang:1.17
</span><span>
</span><span>WORKDIR /usr/src/app
</span><span>
</span><span>COPY go.mod go.sum ./
</span><span>RUN go mod download &amp;&amp; go mod verify
</span><span>
</span><span>COPY . .
</span><span>RUN go build -v -o /usr/local/bin/app ./...
</span><span>
</span><span>CMD ["app"]
</span>
</code></pre>
<p>This allows you to pin the versions of things like the Go compiler without
bothering the sysadmin team to make it available, but in the process you also
don't know what version of the compiler you are actually running. Let's say that
you have all your Docker images built with CI and that CI has an image cache set
up (as is the default in many CI systems). On your laptop you may end up getting
the latest release of Go 1.17 (at the time of writing, this is version 1.17.8),
but since CI may have seen this before and may have an old version of the <code>1.17</code>
tag cached. This would mean that despite your efforts at making things easy to
recreate, you've just accidentally put <a href="https://github.com/golang/go/issues/50165">an ASN.1 parsing
DoS</a> into production, even though
your local machine will never have this issue! Not to mention if the image
you're using has a glibc bug, a DNS parsing bug or any issue with one of the
packages that makes up the image.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; So as a side effect of burning down everything and starting over you don't
actually get a lot of the advantages that the old system had in spite of the
dysfunction?</p>
</div>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; Yep! Realistically though you can get around this by using exact sha256 hashes
of the precise Docker image you want, however this isn't the <em>default</em> behavior
so nobody will really know about it. There are ways to work around this with
tools like Nix, but that is a topic for another day.</p>
</div>

<p>This is what the devops experience feels like, chaining together tools that
require careful handling to avoid accidental security flaws in ways that the
traditional sysadmin team approach fundamentally avoided by design. By
sidestepping the sysadmin team's stability and process, you learn nothing from
what they were doing.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; This is all of course assuming that at the same time as you go devops, you also
avow the grandeur of the cloud. Statistics say that these two usually go hand in
hand as the cloud is sold to executives as good for
devops.</p>
</div>

<p>As for how to get out of this mess though, I'm not sure. Like I said, this is a
<em>social</em> problem that is trying to be solved through a <em>business organizational</em>
fix. I am a technical solutions kind of person and as such I'm really not the
right person to ask about all this. I don't want to propose a solution here.
I've thought out several ideas, but I got nowhere with them fast.</p>
<p>I remember at one of my jobs where I was a devops I ended up also having to be
the tutor on how fundamental parts of the programming language they are using
work. This one service that was handling a lot of production load had issues
where it would just panic and die randomly when a very large customer was trying
to view a list of things that was two orders of magnitude larger than other
customers that use that service. I eventually ended up figuring out where the
issue was but then I had an even harder time explaining what concurrency does at
a fundamental level and how race conditions can make things crash due to
undefined behavior. I think it ended up being a 3 line fix too.</p>
<p>I guess the thing that would really help with this is education and helping
people hone their skills as developers. I understand that there's a learning
curve and not everyone is going to become a programming god overnight, but every
little bit sets off butterfly effects that will ripple down in other ways. Any
solution that requires everyone be a programming god isn't viable for anyone,
including programming gods.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; This whole mentorship thing only really works when the company you work for
doesn't de-facto punish you for mentoring people like that. If you aren't
careful about how you frame this, doing that could make it difficult for you to
prove yourself come review time. "Helped other people do their jobs better"
doesn't really look good for a promotion committee.</p>
</div>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; Yeah but what are you supposed to do if that kind of mentorship is what really
helps motivate you as a person and is what you really enjoy doing? I don't
really see "mentor" as a job title on any postings.</p>
</div>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; There's always getting tired of trying to change things from within and then
writing things out on a publicly visible blog, building up a bunch of articles
over time. Then you could use that body of work as a way to meme yourself into
hiring pipelines thanks to people sharing your links on aggegators like the
orange site. It'd probably help if you also got a reputation as a shitposter,
usually when people are able to openly joke about something that signals that
they are pretty damn experienced in it.</p>
</div>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; You're describing this blog aren't you.</p>
</div>

<p>Like I said though, this is hard. A lot of the problems are actually structural
problems in how companies do the science part of computer science. Structural
problems cannot be solved overnight. These things take time, effort and patience
to truly figure out and in the process you will fail to invent a light bulb many
times over. Devops is probably a necessary evil, but I really wish that
situations weren't toxic enough in the first place to require that evil.</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>

+ 294
- 0
cache/2022/3e8bb1b63246d6f97316864569492382/index.md View File

@@ -0,0 +1,294 @@
title: Technical Solutions Poorly Solve Social Problems
url: https://christine.website/blog/social-quandry-devops-2022-03-17
hash_url: 3e8bb1b63246d6f97316864569492382

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; 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">&lt;<b>Cadey</b>&gt; 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">&lt;<b>Cadey</b>&gt; 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
development and systems administration. On paper this should be great. The silos
will shrink. People will understand the limits and needs of the others. Managers
will be able to have more flexible employees.</p>
<p>Unfortunately though, a lot of the ideas behind devops and the overall
philosophy really do require you to radically burn down everything and start
from scratch. This tends to really not be conducive to engineering timetables
and overall system stability during the age of turbulence.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; What's the problem with burning everything down? Fire cleanses all things and
purifies away the unworthy!</p>
</div>


<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; Not when you're the one being burned!</p>
</div>


<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; Wait, so what actually happens then? Does it just end up being a sysadmin team
made up out of coders?</p>
</div>




<p>Yeah, in practice this ends up being a "new team" or a reboot of an existing
team in ways that is suddenly compelling or sexy to executives because a new
buzzword is on the scene. Realistically, devops did end up getting a proper
definition at a buzzword conference level (being able to handle development and
deployment of services from editor to production), but in practice this ends up
being just some random developers that you tricked into caring about production
now while also telling them that they're better than the sysadmins.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; Two jobs for the price of one!</p>
</div>

<p>This ends up shafting the sysadmin team even harder because the new fancy devops
team has things they can talk about as positives for their quarters, so people
can more easily make a case for promotion. As a sysadmin, your "success" case is
"bad things didn't happen", which means success can't stand out on reviews.
Consider "scaled production above the rate of our customer acquistion rate"
against "set up continuous delivery to ensure velocity on our team, saving 50
hours of effort per week". Which one of those do you think gets you promoted?
Which one of those do you think gets headcount for new hires?</p>
<p>This has human costs too. At one of my past jobs doing more sysadmin-y things
(it was marketed as a devops hybrid role, but the "hybrid" part was more of
"frantically patch up the sinking ship with code" and not traditional software
development). Sleep is really essential to helping you function properly to do
your job. During the times when I was pager bitch, there was at least a 1/8
chance that I would be woken up in the middle of the night to handle a problem.
I had to change my pager tone 15 times and still get goosebumps hearing those
old sounds nearly a decade later. This ended up being a huge factor in my
developing anxiety issues that I still feel today. I ended up getting addicted
to weed really bad for a few years. I admit that I'm really not the most robust
person in the world, but these things add up.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; I guess "addicted to weed" isn't totally accurate or inaccurate here, it's more
that I was addicted to the feeling of being high rather than dependence on the
drug itself. Either way, it was bad and weed was my cope. It also probably
really didn't help that I was also starting hormone replacement therapy at the
time, so I was going through second puberty at the time as well. This is the
kind of human capital cost when dealing with dysfunction like this. I've always
been kind of afraid to speak up about this.</p>
</div>

<p>However, there are real technical problems that can only really be solved from a
devops perspective. Tools like Docker would probably never have happened in the
way they did if the devops philosophy didn't exist.</p>
<p><img src="https://cdn.christine.website/file/christine-static/blog/1BDBBB94-7052-4E4C-AE32-CFEE4226CBA8.jpeg" alt="A three panel meme with an old man talking to a child. The child says &quot;it works on my machine&quot;. The old man replies with &quot;then we'll ship your machine&quot;. The last panel says &quot;and that is how docker was born&quot;."></p>
<p>In a way, Docker is one of the perfect examples of the devops philosophy. It
allows developers to have their own custom versions of everything. They can use
custom compilers that the sysadmins don't have to integrate into everything.
They can experiment with new toolstacks, languages and build systems without
worrying about how they integrate into existing processes. And in the process it
defaults to things that are so hilariously unsafe that you only really realize
the problems when they own you. It makes it easy to ship around configurations
for services yes, but it doesn't make supply chain management easy at all.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; Wait, what about that? How does that make any sense?</p>
</div>

<p>Okay, let's consider this basic Dockerfile that builds a Go service. If you
start from very little knowledge of what's going on, you'd probably end up with
something like this:</p>
<pre><code class="language-Dockerfile">
<span>FROM golang:1.17
</span><span>
</span><span>WORKDIR /usr/src/app
</span><span>
</span><span>COPY go.mod go.sum ./
</span><span>RUN go mod download &amp;&amp; go mod verify
</span><span>
</span><span>COPY . .
</span><span>RUN go build -v -o /usr/local/bin/app ./...
</span><span>
</span><span>CMD ["app"]
</span>
</code></pre>
<p>This allows you to pin the versions of things like the Go compiler without
bothering the sysadmin team to make it available, but in the process you also
don't know what version of the compiler you are actually running. Let's say that
you have all your Docker images built with CI and that CI has an image cache set
up (as is the default in many CI systems). On your laptop you may end up getting
the latest release of Go 1.17 (at the time of writing, this is version 1.17.8),
but since CI may have seen this before and may have an old version of the <code>1.17</code>
tag cached. This would mean that despite your efforts at making things easy to
recreate, you've just accidentally put <a href="https://github.com/golang/go/issues/50165">an ASN.1 parsing
DoS</a> into production, even though
your local machine will never have this issue! Not to mention if the image
you're using has a glibc bug, a DNS parsing bug or any issue with one of the
packages that makes up the image.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; So as a side effect of burning down everything and starting over you don't
actually get a lot of the advantages that the old system had in spite of the
dysfunction?</p>
</div>


<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; Yep! Realistically though you can get around this by using exact sha256 hashes
of the precise Docker image you want, however this isn't the <em>default</em> behavior
so nobody will really know about it. There are ways to work around this with
tools like Nix, but that is a topic for another day.</p>
</div>

<p>This is what the devops experience feels like, chaining together tools that
require careful handling to avoid accidental security flaws in ways that the
traditional sysadmin team approach fundamentally avoided by design. By
sidestepping the sysadmin team's stability and process, you learn nothing from
what they were doing.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; This is all of course assuming that at the same time as you go devops, you also
avow the grandeur of the cloud. Statistics say that these two usually go hand in
hand as the cloud is sold to executives as good for
devops.</p>
</div>

<p>As for how to get out of this mess though, I'm not sure. Like I said, this is a
<em>social</em> problem that is trying to be solved through a <em>business organizational</em>
fix. I am a technical solutions kind of person and as such I'm really not the
right person to ask about all this. I don't want to propose a solution here.
I've thought out several ideas, but I got nowhere with them fast.</p>
<p>I remember at one of my jobs where I was a devops I ended up also having to be
the tutor on how fundamental parts of the programming language they are using
work. This one service that was handling a lot of production load had issues
where it would just panic and die randomly when a very large customer was trying
to view a list of things that was two orders of magnitude larger than other
customers that use that service. I eventually ended up figuring out where the
issue was but then I had an even harder time explaining what concurrency does at
a fundamental level and how race conditions can make things crash due to
undefined behavior. I think it ended up being a 3 line fix too.</p>
<p>I guess the thing that would really help with this is education and helping
people hone their skills as developers. I understand that there's a learning
curve and not everyone is going to become a programming god overnight, but every
little bit sets off butterfly effects that will ripple down in other ways. Any
solution that requires everyone be a programming god isn't viable for anyone,
including programming gods.</p>

<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; This whole mentorship thing only really works when the company you work for
doesn't de-facto punish you for mentoring people like that. If you aren't
careful about how you frame this, doing that could make it difficult for you to
prove yourself come review time. "Helped other people do their jobs better"
doesn't really look good for a promotion committee.</p>
</div>


<div class="conversation">

<p class="conversation-chat">&lt;<b>Mara</b>&gt; Yeah but what are you supposed to do if that kind of mentorship is what really
helps motivate you as a person and is what you really enjoy doing? I don't
really see "mentor" as a job title on any postings.</p>
</div>


<div class="conversation">

<p class="conversation-chat">&lt;<b>Numa</b>&gt; There's always getting tired of trying to change things from within and then
writing things out on a publicly visible blog, building up a bunch of articles
over time. Then you could use that body of work as a way to meme yourself into
hiring pipelines thanks to people sharing your links on aggegators like the
orange site. It'd probably help if you also got a reputation as a shitposter,
usually when people are able to openly joke about something that signals that
they are pretty damn experienced in it.</p>
</div>


<div class="conversation">

<p class="conversation-chat">&lt;<b>Cadey</b>&gt; You're describing this blog aren't you.</p>
</div>

<p>Like I said though, this is hard. A lot of the problems are actually structural
problems in how companies do the science part of computer science. Structural
problems cannot be solved overnight. These things take time, effort and patience
to truly figure out and in the process you will fail to invent a light bulb many
times over. Devops is probably a necessary evil, but I really wish that
situations weren't toxic enough in the first place to require that evil.</p>

+ 188
- 0
cache/2022/4bf828ef0ce7191d048d0c510a3c3e0c/index.html View File

@@ -0,0 +1,188 @@
<!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>☕️ Journal : Marges (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://thom4.net/2022/03/23/marges/">

<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>☕️ Journal : Marges</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://thom4.net/2022/03/23/marges/" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p>Dans [Décolonialité et Privilège] de Rachel Borghi, elle écrit qu’une action possible des personnes privilégiées pourrait être <q>d’abandonner le centre pour les marges et [pour] renoncer à l’autorité</q>.</p>
<p>J’ai rarement aimé “le centre”. Celui où il fallait aller (“rester dans le rang”). Le podium. La scène. Le tableau de classe.</p>
<p>J’aimais le fond de la classe près du radiateur. Mes 2-3 ami·es en classe — les geeks, les nerds. Le coin le plus calme en soirée.</p>
<p>J’aime la notion d’<a target="_blank" rel="noopener" href="https://fr.wiktionary.org/wiki/inframince">inframince</a>. J’aime les “membranes” photographiques de Depardon — ces espaces où la ville devient campagne, où le ciel rejoint la terre, où le vide s’écarte du plein, de la joie à la tristesse. Des marges conceptuelles.</p>
<p>J’ai toujours trouvé étrange le passage de frontières — qu’elles soient départements, régions ou pays. On passe “ailleurs”, sans que rien ne change, dans une continuité.</p>
<hr>
<p>Bien sûr Rachel évoque un autre “centre”. Celui de la norme qui ne se (dé)nomme pas. Celui qui énonce et dicte. Celui qui invalide ce qui n’est pas valide. Celui qui est Pouvoir.</p>
<hr>
<p>Mon plus grand soulagement quand la <a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=NVpH1w_aSUk">startup Dijiwan s’est effondrée</a> — ça fera 10 ans cet été — c’était qu’on n’allait plus accélérer la captation de valeur chez les entreprises clientes.</p>
<p>Je n’allais plus contribuer à ce que d’autres — au centre — s’enrichissent grâce à un capital numérique.</p>
<hr>
<p>Pour autant, j’ai des fois du mal à me sentir bien à la marge. Marge. Marginal. Ça remet en lumière que je peux être plus au centre que d’autres à la marge. En marge du centre, et en marge de la marge.</p>
<p>Pour autant, je conscientise aimer cultiver la marge. Me questionner. Choisir là où c’était déjà choisi pour moi, pour ce que je représente. Me constituer un chemin congruent. Vérifier que je ne glisse pas vers le centre.</p>
<p>Vérifier que je peux être à la marge, <em>et</em> au centre de ma vie. Ce que j’ai choisi et qui ne marginalise pas d’autres personnes. Vérifier les impacts, positifs <em>et</em> négatifs.</p>
<hr>
<p>Un développeur web très au centre, ça serait une personne qui est salariée dans une entreprise financée par de la dette (une start-up), dont le produit “œuvre pour un monde meilleur” et dont certains des clients, directs ou indirects, représentent les énergies fossiles ou des industries polluantes. Cette personne se moquerait de ses camarades, ne s’intéresserait pas aux inégalités de son équipe, de l’utilisation/traitement/revente des données, et prendrait l’avion pour aller à des conférences ou en séminaire d’équipe.</p>
<p>Un développeur à la marge… y’a de la marge !</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>

+ 21
- 0
cache/2022/4bf828ef0ce7191d048d0c510a3c3e0c/index.md View File

@@ -0,0 +1,21 @@
title: ☕️ Journal : Marges
url: https://thom4.net/2022/03/23/marges/
hash_url: 4bf828ef0ce7191d048d0c510a3c3e0c

<p>Dans [Décolonialité et Privilège] de Rachel Borghi, elle écrit qu’une action possible des personnes privilégiées pourrait être <q>d’abandonner le centre pour les marges et [pour] renoncer à l’autorité</q>.</p>
<p>J’ai rarement aimé “le centre”. Celui où il fallait aller (“rester dans le rang”). Le podium. La scène. Le tableau de classe.</p>
<p>J’aimais le fond de la classe près du radiateur. Mes 2-3 ami·es en classe — les geeks, les nerds. Le coin le plus calme en soirée.</p>
<p>J’aime la notion d’<a target="_blank" rel="noopener" href="https://fr.wiktionary.org/wiki/inframince">inframince</a>. J’aime les “membranes” photographiques de Depardon — ces espaces où la ville devient campagne, où le ciel rejoint la terre, où le vide s’écarte du plein, de la joie à la tristesse. Des marges conceptuelles.</p>
<p>J’ai toujours trouvé étrange le passage de frontières — qu’elles soient départements, régions ou pays. On passe “ailleurs”, sans que rien ne change, dans une continuité.</p>
<hr>
<p>Bien sûr Rachel évoque un autre “centre”. Celui de la norme qui ne se (dé)nomme pas. Celui qui énonce et dicte. Celui qui invalide ce qui n’est pas valide. Celui qui est Pouvoir.</p>
<hr>
<p>Mon plus grand soulagement quand la <a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=NVpH1w_aSUk">startup Dijiwan s’est effondrée</a> — ça fera 10 ans cet été — c’était qu’on n’allait plus accélérer la captation de valeur chez les entreprises clientes.</p>
<p>Je n’allais plus contribuer à ce que d’autres — au centre — s’enrichissent grâce à un capital numérique.</p>
<hr>
<p>Pour autant, j’ai des fois du mal à me sentir bien à la marge. Marge. Marginal. Ça remet en lumière que je peux être plus au centre que d’autres à la marge. En marge du centre, et en marge de la marge.</p>
<p>Pour autant, je conscientise aimer cultiver la marge. Me questionner. Choisir là où c’était déjà choisi pour moi, pour ce que je représente. Me constituer un chemin congruent. Vérifier que je ne glisse pas vers le centre.</p>
<p>Vérifier que je peux être à la marge, <em>et</em> au centre de ma vie. Ce que j’ai choisi et qui ne marginalise pas d’autres personnes. Vérifier les impacts, positifs <em>et</em> négatifs.</p>
<hr>
<p>Un développeur web très au centre, ça serait une personne qui est salariée dans une entreprise financée par de la dette (une start-up), dont le produit “œuvre pour un monde meilleur” et dont certains des clients, directs ou indirects, représentent les énergies fossiles ou des industries polluantes. Cette personne se moquerait de ses camarades, ne s’intéresserait pas aux inégalités de son équipe, de l’utilisation/traitement/revente des données, et prendrait l’avion pour aller à des conférences ou en séminaire d’équipe.</p>
<p>Un développeur à la marge… y’a de la marge !</p>

+ 191
- 0
cache/2022/5eb0016b355ac4b358be367fe64f4c84/index.html View File

@@ -0,0 +1,191 @@
<!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>Mourning Loss of a Team Member as a Remote Team (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://www.sofuckingagile.com/blog/mourning-loss-as-a-remote-team">

<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>Mourning Loss of a Team Member as a Remote Team</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://www.sofuckingagile.com/blog/mourning-loss-as-a-remote-team" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p>This is a hard one to write about. Last year our team lost Pete, a long time engineer. Pete took his own life. He had battled mental health issues for some time. </p>
<p>Our team was tight, fully remote for a decade, and Pete was part of it for 7 years. In the software engineering world, 7 years at the same job is a lifetime. The thing was, we’d never met Pete. Only a few of us had actually met in person. He was several time-zones away and I don’t think I could pick him out of a lineup. When we connected for meetings, we didn’t use cameras so I’d only seen a few images of Pete in family photos that we’d shared between us. I was the person who hired him and even during the interview process we didn’t use cameras. Nonetheless, I felt like we were in sync, we were friends, and he was a crucial part of the team. We talked frequently about music, family, video games and his love of Disney. I learned about his local politics and his views on the world. I loved Pete.</p>
<h2>His Wife Had to Create a Support Ticket</h2>
<p>We fucked up. We had no connection to the people in Pete’s life, like his wife and kids. Pete was a full time contractor. This was a typical arrangement for over half of our team. We were scattered all over the world and we got started with a global team in the easiest way, which was hiring engineers as contractors. Pete had no HR, no health benefits, and no employee record with alternate or emergency contacts. We had 600 people in the company, but he was only known to about 10. And from the perspective of his family, they only knew the name of the company he was contracted with and my first name, but nothing else.</p>
<p>When he passed away, his wife had no way to contact me or anyone on his team. When I arrived in the morning, I got a message from our customer support team lead. Pete’s wife had used the technical support chat to get a message to me. She was put in a queue with every other customer user who couldn’t login or forgotten how to access the mobile app. I was gutted by the eventual news and by the fact that Pete’s passing had become a support ticket. This made it so much more devastating.</p>
<p>If you work with full time remote employees or contractors, please put channels in place to communicate with family or alternate contacts. Make sure emergency contact info is shared on both sides.</p>
<h2>Make Sure the Broader Organization Knows You Are Mourning</h2>
<p>The team was lost. Because he was a full time contractor and not an employee, there was no HR intervention to help. I honestly don’t know if HR does help in these situations, but I like to think they could schedule grief counseling or something. We initially didn’t know what to do, or how to mourn.</p>
<p>We started by letting the organization know that we were all struggling. We ran this all the way up to the CEO. Because of his remote contractor arrangement, nobody outside of our team would have known that anything happened. We’d be postponing releases, this sprint was fucked, the next one too and maybe more to come. We needed time to pull it together. We’d lost a real one. </p>
<p>Messaging the broader company and other teams was key. Some people are pros at what to do in these situations and they can operate with a clear head. Before long we had UberEats delivering to Pete’s family on the other side of the world, kind memories and words circulating and we had donations going to Pete’s local mental health organization. Our team, in our current state, could not have pulled this together without the help of the broader company.</p>
<h2>/Pete Easter Egg</h2>
<p>As a squad, we decided the best memorial was an Easter Egg in the app. Pete was all over this app. He was prolific. This team and teams to come would be maintaining his code for years.</p>
<p>Pete deserved his own route. We created<strong> /Pete</strong> and put a page there memorializing him in code. This wasn’t a traditional hard-to-find easter egg. It was top level. Just add /Pete to your address bar and you’re there. This seemed right. </p>
<h2>Did We Ignore the Signs?</h2>
<p>It’s easy to think that we could have prevented Pete’s death. Our team spent the most time with him on a daily basis. I remember doing a ‘share photos of your workspace’ with the team, and being shocked by Pete’s workspace. 3 keyboards stacked on top of each other, heaps of peripherals, old broken monitors, headphones, piles of trash, a total mess. After seeing this, I began to learn a little bit about the scope of Pete’s challenges.</p>
<p>Pete would ask to work more hours. He claimed he could use the money. He was a contractor remember, so more hours means more money, and I could reconcile this without thinking twice. Who doesn’t want more money? After he passed I learned that work was a distraction for him. It gave him something to obsess over and he could think less about the mental and emotional struggles that were plaguing him. His wife sent photos of a bunch of handwritten notes of JavaScript that were lying around his room. She had went looking for a note, a clue as to why. She said ‘<em>maybe you can make sense of these</em>.’ I couldn’t, but I knew people who could. It was an iframe dynamic height function. It was work.</p>
<p>Could I have pushed harder to make him an employee? Prior to getting acquired, the process was underway to sponsor Pete to become an employee. After the acquisition, this sort of stalled and wasn’t something our new owners had interest in at the time. I could have pushed harder. I don’t know if it would have helped, but the thought still lingers that some things may have helped, such as having HR oversight, health benefits, and mental health affordances.</p>
<h2>Caught a Wobbler</h2>
<p>Pete was Scottish and he had his own slang which I loved and miss dearly. When he found a bug, he’d say ‘<em>Caught a wobbler!</em>’ I loved this. Pay attention to your team. Build closeness. Get to know about everyone’s family and private life. Take mental health seriously and talk openly about it. It may seem like prying, but you might catch a wobbler with a team member that you can address early.</p>
<p>RIP Pete.</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>

+ 24
- 0
cache/2022/5eb0016b355ac4b358be367fe64f4c84/index.md View File

@@ -0,0 +1,24 @@
title: Mourning Loss of a Team Member as a Remote Team
url: https://www.sofuckingagile.com/blog/mourning-loss-as-a-remote-team
hash_url: 5eb0016b355ac4b358be367fe64f4c84

<p>This is a hard one to write about. Last year our team lost Pete, a long time engineer. Pete took his own life. He had battled mental health issues for some time. </p>
<p>Our team was tight, fully remote for a decade, and Pete was part of it for 7 years. In the software engineering world, 7 years at the same job is a lifetime. The thing was, we’d never met Pete. Only a few of us had actually met in person. He was several time-zones away and I don’t think I could pick him out of a lineup. When we connected for meetings, we didn’t use cameras so I’d only seen a few images of Pete in family photos that we’d shared between us. I was the person who hired him and even during the interview process we didn’t use cameras. Nonetheless, I felt like we were in sync, we were friends, and he was a crucial part of the team. We talked frequently about music, family, video games and his love of Disney. I learned about his local politics and his views on the world. I loved Pete.</p>
<h2>His Wife Had to Create a Support Ticket</h2>
<p>We fucked up. We had no connection to the people in Pete’s life, like his wife and kids. Pete was a full time contractor. This was a typical arrangement for over half of our team. We were scattered all over the world and we got started with a global team in the easiest way, which was hiring engineers as contractors. Pete had no HR, no health benefits, and no employee record with alternate or emergency contacts. We had 600 people in the company, but he was only known to about 10. And from the perspective of his family, they only knew the name of the company he was contracted with and my first name, but nothing else.</p>
<p>When he passed away, his wife had no way to contact me or anyone on his team. When I arrived in the morning, I got a message from our customer support team lead. Pete’s wife had used the technical support chat to get a message to me. She was put in a queue with every other customer user who couldn’t login or forgotten how to access the mobile app. I was gutted by the eventual news and by the fact that Pete’s passing had become a support ticket. This made it so much more devastating.</p>
<p>If you work with full time remote employees or contractors, please put channels in place to communicate with family or alternate contacts. Make sure emergency contact info is shared on both sides.</p>
<h2>Make Sure the Broader Organization Knows You Are Mourning</h2>
<p>The team was lost. Because he was a full time contractor and not an employee, there was no HR intervention to help. I honestly don’t know if HR does help in these situations, but I like to think they could schedule grief counseling or something. We initially didn’t know what to do, or how to mourn.</p>
<p>We started by letting the organization know that we were all struggling. We ran this all the way up to the CEO. Because of his remote contractor arrangement, nobody outside of our team would have known that anything happened. We’d be postponing releases, this sprint was fucked, the next one too and maybe more to come. We needed time to pull it together. We’d lost a real one. </p>
<p>Messaging the broader company and other teams was key. Some people are pros at what to do in these situations and they can operate with a clear head. Before long we had UberEats delivering to Pete’s family on the other side of the world, kind memories and words circulating and we had donations going to Pete’s local mental health organization. Our team, in our current state, could not have pulled this together without the help of the broader company.</p>
<h2>/Pete Easter Egg</h2>
<p>As a squad, we decided the best memorial was an Easter Egg in the app. Pete was all over this app. He was prolific. This team and teams to come would be maintaining his code for years.</p>
<p>Pete deserved his own route. We created<strong> /Pete</strong> and put a page there memorializing him in code. This wasn’t a traditional hard-to-find easter egg. It was top level. Just add /Pete to your address bar and you’re there. This seemed right. </p>
<h2>Did We Ignore the Signs?</h2>
<p>It’s easy to think that we could have prevented Pete’s death. Our team spent the most time with him on a daily basis. I remember doing a ‘share photos of your workspace’ with the team, and being shocked by Pete’s workspace. 3 keyboards stacked on top of each other, heaps of peripherals, old broken monitors, headphones, piles of trash, a total mess. After seeing this, I began to learn a little bit about the scope of Pete’s challenges.</p>
<p>Pete would ask to work more hours. He claimed he could use the money. He was a contractor remember, so more hours means more money, and I could reconcile this without thinking twice. Who doesn’t want more money? After he passed I learned that work was a distraction for him. It gave him something to obsess over and he could think less about the mental and emotional struggles that were plaguing him. His wife sent photos of a bunch of handwritten notes of JavaScript that were lying around his room. She had went looking for a note, a clue as to why. She said ‘<em>maybe you can make sense of these</em>.’ I couldn’t, but I knew people who could. It was an iframe dynamic height function. It was work.</p>
<p>Could I have pushed harder to make him an employee? Prior to getting acquired, the process was underway to sponsor Pete to become an employee. After the acquisition, this sort of stalled and wasn’t something our new owners had interest in at the time. I could have pushed harder. I don’t know if it would have helped, but the thought still lingers that some things may have helped, such as having HR oversight, health benefits, and mental health affordances.</p>
<h2>Caught a Wobbler</h2>
<p>Pete was Scottish and he had his own slang which I loved and miss dearly. When he found a bug, he’d say ‘<em>Caught a wobbler!</em>’ I loved this. Pay attention to your team. Build closeness. Get to know about everyone’s family and private life. Take mental health seriously and talk openly about it. It may seem like prying, but you might catch a wobbler with a team member that you can address early.</p>
<p>RIP Pete.</p>

+ 236
- 0
cache/2022/a863c20d0cb9722df74219009e8365a3/index.html View File

@@ -0,0 +1,236 @@
<!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>Jakarta’s Transit Miracle (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://infiniteblock.substack.com/p/jakartas-transit-miracle">

<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>Jakarta’s Transit Miracle</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://infiniteblock.substack.com/p/jakartas-transit-miracle" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p>Welcome 👋</p>
<p>You’re reading Infinite Block, an email missive covering innovation at the intersection of cities, policy, and technology. </p>
<p>Today’s post is about how Jakarta, the fast-growing capital of Indonesia, went from being one of the most-polluted and -congested cities in the world to a budding transit-rich megalopolis almost overnight. What factors explain Jakarta’s sudden success and how can other cities that are seeking to roll back car dependency emulate it? <strong>Scroll down to find out.</strong></p>
<ul><li><p>The Infinite Block podcast is now available on <strong><a href="https://podcasts.apple.com/us/podcast/infinite-block/id1611976523" rel="">Apple</a></strong> and <strong><a href="https://open.spotify.com/show/5uwPwwzi3yEglaWCUkluFj" rel="">Spotify</a></strong>. Our first episode is an in-depth conversation about the history and future of cities with world-renowned tech analyst Horace Dediu. Give it a <a href="https://www.podbean.com/media/share/pb-ure9y-11ab3e4" rel="">listen</a> and stay tuned for more episodes soon. </p></li><li><p>Lastly, today’s post is contributed by the excellent <strong><a href="https://twitter.com/CityBits_Max" rel="">Max Kim</a></strong>, a passionate urbanist who loves anything to do with cities. He runs a newsletter on urban trends and city success stories called <strong><a href="https://citybits.substack.com/" rel="">CityBits</a></strong>, and is also on Twitter at <strong><a href="https://twitter.com/CityBits_Max" rel="">@CityBits_Max</a></strong></p></li></ul>
<p><hr></p>
<h1>Jakarta’s Transit Miracle</h1>
<p><em>By <a href="https://twitter.com/CityBits_Max" rel="">Max Kim</a></em></p>
<p>Today we’re talking about Jakarta. The capital of Indonesia is significant for many reasons. It’s the most populous city in Southeast Asia with 31M residents living in the greater metro area, it’s home to one of <a href="https://www.javajazzfestival.com/" rel="">the largest jazz festivals in the world</a>, and its official nickname is “The Big <a href="https://www.google.com/search?q=durian&amp;rlz=1C1CHBF_enUS867US867&amp;sxsrf=APq-WBsbPo7YedDvoIeaL2-mXG9G47vgwA:1646767827666&amp;source=lnms&amp;tbm=isch&amp;sa=X&amp;ved=2ahUKEwiD0pSboLf2AhXPknIEHVgmD1sQ_AUoAXoECAIQAw" rel="">Durian</a>,” an homage to New York City’s “Big Apple.” But today we’re not going to talk about any of that. </p>
<p>Instead we’ll focus on the incredible transformation that Jakarta has undergone in the last ~10 years, during which it went from being one of the <a href="https://www.theguardian.com/cities/2016/nov/23/world-worst-traffic-jakarta-alternative" rel="">most-congested and -polluted cities in the world</a> to a global leader in public transit and cycling.</p>
<p>In this article I’ll cover just how Jakarta’s transformation came to be, how the local government has been able to maintain this success, and what other cities can learn from it. Let’s dive in.</p>
<h2><strong>The good</strong></h2>
<p>To start off, let’s look at what Jakarta has actually accomplished. Since the mid-2010s, the city has made tremendous improvements in public transit, cycling, and people-centered urban design:</p>
<p>Now these stats are noteworthy on their own, but they become even more impressive when we understand where Jakarta was just a few years ago. </p>
<h3><strong>Setting the scene</strong></h3>
<p>You see, Jakarta was not always such a progressive transportation city. Despite a majority of Jakartans not owning a car and <a href="https://www.itdp.org/2021/05/06/jakarta-is-what-resiliency-looks-like/" rel="">less than 10% of the population</a> commuting via private vehicle, as recently as 2015 the city had some of the<a href="https://time.com/3695068/worst-cities-traffic-jams/" rel=""> worst traffic in the world</a>, and it still regularly <a href="https://www.channelnewsasia.com/asia/indonesia-jakarta-air-pollution-emissions-vehicles-factories-2285926#:~:text=According%20to%20air%20quality%20monitoring,quality%20was%20deemed%20%E2%80%9Chealthy%E2%80%9D." rel="">ranks as one of the most polluted cities </a>globally. </p>
<p>Now some might argue that these problems are due to <a href="https://www.macrotrends.net/cities/21454/jakarta/population?q=Jakarta%2C+Indonesia+Metro+Area+Population+1950-2022" rel="">Jakarta’s significant population growth over the past few decades</a>. And that certainly is a factor, but it’s probably not the only reason since plenty of other cities around the world saw similar increases in population size <strong>without </strong>corresponding spikes in congestion and pollution. So if not population growth, what explains all the aforementioned issues with Jakarta’s transportation systems? A few reasons: </p>
<ul><li><p><strong>Little support for walking/cycling:</strong> As recently as 2019, cycling advocates still criticized <a href="https://www.channelnewsasia.com/asia/grim-future-for-jakarta-cyclists-as-cars-dominate-traffic-lanes-897971" rel="">Jakarta’s scattered, poorly maintained bike lanes</a> and low levels of walkability </p></li><li><p><strong>Lack of a subway/metro: </strong>Unlike most other major metropolitan areas, Jakarta lacked a metro (subway or MRT) until 2019</p></li><li><p><strong>Disorganized, unconsolidated bus services:</strong> While Jakarta does have its own BRT system called Transjakarta, for many years, it did not service broad swaths of the city, and worse, it frequently clashed with local, independent bus fleets</p></li></ul>
<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" rel="nofollow" href="https://cdn.substack.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F56897985-03f5-473a-8b27-a614c9b26993_1080x1420.jpeg"><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F56897985-03f5-473a-8b27-a614c9b26993_1080x1420.jpeg" data-attrs='{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/56897985-03f5-473a-8b27-a614c9b26993_1080x1420.jpeg","fullscreen":null,"imageSize":null,"height":1420,"width":1080,"resizeWidth":null,"bytes":null,"alt":null,"title":null,"type":null,"href":null}' class="sizing-normal" alt="" srcset="https://cdn.substack.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F56897985-03f5-473a-8b27-a614c9b26993_1080x1420.jpeg 424w, https://cdn.substack.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F56897985-03f5-473a-8b27-a614c9b26993_1080x1420.jpeg 848w, https://cdn.substack.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F56897985-03f5-473a-8b27-a614c9b26993_1080x1420.jpeg 1272w, https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F56897985-03f5-473a-8b27-a614c9b26993_1080x1420.jpeg 1456w" sizes="100vw"></a><figcaption class="image-caption">Jakarta be kidding me!</figcaption></figure></div>
<p>So just to recap: In this booming SE Asian city we have some of the world’s worst congestion and pollution, no subway, MRT, or light rail to speak of, poor cycling/walking conditions, and insufficient or unsatisfactory bus service. It’s no surprise, then, that road traffic accidents were <a href="https://www.thejakartapost.com/news/2017/01/23/traffic-accidents-remain-major-contributor-to-fatalities-health-problems.html" rel="">a leading cause of death in Jakarta in the 2010s</a>.</p>
<p>If you looked at Jakarta 10 years ago you probably wouldn’t have predicted it would ever grow into the transit-heavy, cycling-friendly city it is today.</p>
<p>But… it did! </p>
<p>So how did Jakarta shake itself free of this automobile-induced stupor? What really caused this mobility revolution? For my money the main reasons are simple:</p>
<ol><li><p>The government is all in on supporting a mobility transformation that reduces traffic</p></li><li><p>Civic leaders have made significant efforts to integrate the city’s various travel modes, making sure they all complement each other</p></li></ol>
<h3><strong>Top-down leadership</strong></h3>
<p>The first reason for Jakarta’s success is that the government actually believes in the mission! Like we’ve seen in other cities that have experienced large-scale transit and mobility successes, from Bogotá’s adoption of the largest <a href="https://www.planetizen.com/news/2021/05/113435-looking-future-transmilenio-turns-20" rel="">BRT in South America under Mayor Enrique Peñalosa</a> to Paris’ steady move <a href="https://slate.com/business/2021/09/paris-cars-bicycles-walking-david-belliard-anne-hidalgo.html" rel="">away from car-dependency under Mayor Anne Hidalgo</a>, these kind of large infrastructural changes <strong>are often most effective when the government truly believes in their value</strong>. This isn’t to diminish the importance of grassroots movements, but just to emphasize that without government support, it’s much harder to create change, especially on a city-wide scale. </p>
<p>It's telling that Governor Anies Baswedan, the top public official in Jakarta, often <a href="https://en.antaranews.com/news/202785/baswedan-pushes-disabled-friendly-public-transportation" rel="">makes announcements</a> about <a href="https://en.antaranews.com/news/215889/jakarta-aims-to-lead-in-sustainable-transportation-governor" rel="">mobility policies</a> and <a href="https://en.antaranews.com/news/215889/jakarta-aims-to-lead-in-sustainable-transportation-governor" rel="">public transit improvements</a> himself, rather than outsourcing the task to a less-high ranking official. The message is clear: Jakarta’s highest-ranking leader understands the importance of good transportation.</p>
<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" rel="nofollow" href="https://cdn.substack.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2bba692e-6367-44ac-b1e3-52c416059e2b_500x281.gif"><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2bba692e-6367-44ac-b1e3-52c416059e2b_500x281.gif" data-attrs='{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/2bba692e-6367-44ac-b1e3-52c416059e2b_500x281.gif","fullscreen":null,"imageSize":null,"height":281,"width":500,"resizeWidth":null,"bytes":null,"alt":null,"title":null,"type":null,"href":null}' class="sizing-normal" alt="" srcset="https://cdn.substack.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2bba692e-6367-44ac-b1e3-52c416059e2b_500x281.gif 424w, https://cdn.substack.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2bba692e-6367-44ac-b1e3-52c416059e2b_500x281.gif 848w, https://cdn.substack.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2bba692e-6367-44ac-b1e3-52c416059e2b_500x281.gif 1272w, https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2bba692e-6367-44ac-b1e3-52c416059e2b_500x281.gif 1456w" sizes="100vw"></a><figcaption class="image-caption">Governor Anies Baswedan and his cabinet</figcaption></figure></div>
<p>For example, during the height of the pandemic the government issued <a href="https://www.iccc.or.id/wp-content/uploads/2020/08/DKI-Jakarta-Governor-Regulation-51-of-2020-SSEK-Translation.pdf" rel="">Governor Regulation #51 of 2020, Article 21</a>, which reads:</p>
<blockquote><p><em>“All road segments are prioritized for pedestrians and bicycle transport users as a means of daily mobility for accessible distances.”</em></p></blockquote>
<p>Surprisingly succinct wording for a government policy, but the message sums up the city’s goals quite nicely. It’s one thing for a city to build a few pop-up bike lanes. It’s another for them to go the extra mile (or kilometer, in Jakarta)<strong> and officially prioritize road space for non-drivers.</strong></p>
<h3><strong>Integration of mobility options</strong></h3>
<p>Now government support is helpful, but big problems require more than political willpower alone. You also need to have a strategy. This brings us to the second major reason for the city’s recent successes, <strong>the integration of its various mobility services. </strong></p>
<p>You see, until recently one of the biggest issues facing the city was that its various transportation systems (cycling/bikeshare, BRT, minibuses, MRT) <strong>were managed separately and didn’t really interact with each other on an official level.</strong> </p>
<ul><li><p>Competition over bus routes between Transjakarta and privately run fleets caused frequent, sometimes violent <a href="http://cakrawikara.id/publikasi/artikel/minibus-and-transjakarta-transport-wars/" rel="">protests throughout the 2010s</a>.</p></li><li><p>MRT stations formerly did not connect with commuter rail or bus stops, making trip-chaining difficult for commuters</p></li><li><p>And while people wanted to cycle, until recently the city lacked basic infrastructure, like bicycle parking/storage at MRT stations, bike racks on the front of buses, sufficient bikeshare stations, etc. <strong>that would allow these different modes of transportation to complement and communicate with each other. </strong></p></li></ul>
<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" rel="nofollow" href="https://cdn.substack.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60a2a39d-825c-4adb-a22c-342a3e6c2f58_1065x596.jpeg"><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60a2a39d-825c-4adb-a22c-342a3e6c2f58_1065x596.jpeg" data-attrs='{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/60a2a39d-825c-4adb-a22c-342a3e6c2f58_1065x596.jpeg","fullscreen":null,"imageSize":null,"height":596,"width":1065,"resizeWidth":null,"bytes":null,"alt":null,"title":null,"type":null,"href":null}' class="sizing-normal" alt="" srcset="https://cdn.substack.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60a2a39d-825c-4adb-a22c-342a3e6c2f58_1065x596.jpeg 424w, https://cdn.substack.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60a2a39d-825c-4adb-a22c-342a3e6c2f58_1065x596.jpeg 848w, https://cdn.substack.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60a2a39d-825c-4adb-a22c-342a3e6c2f58_1065x596.jpeg 1272w, https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60a2a39d-825c-4adb-a22c-342a3e6c2f58_1065x596.jpeg 1456w" sizes="100vw"></a></figure></div>
<p>In response, the government launched a program called Jak Lingko in 2017 to integrate and optimize the city’s various transportation options. (In case you’re wondering, the name comes from “Jak,” as in Jakarta, and “Lingko,” <a href="https://www.thejakartapost.com/news/2018/10/08/ok-otrip-becomes-jak-lingko-expands-network.html" rel="">a network of interconnected irrigation systems used in the southernmost province of Indonesia</a>.) This concerted effort was conceived to increase communication between transit agencies and operators. <strong>By aggregating and viewing the transit system holistically, the goal was to reduce inefficiency and remove redundancies. </strong></p>
<p>For example, a key part of BRT’s recent success has been increased cooperation with the privately-run minibus fleets. Rather than jockeying for territory, Transjakarta is now incorporating small and medium-sized buses, which can better navigate narrow streets and reach more remote areas, thus expanding its coverage network. The resulting aggregation ended up almost <a href="https://www.youtube.com/watch?v=6v75n2WniZ0&amp;t=2317s" rel="">doubling Transjakarta’s coverage from 42% to 82%</a> in just four years.</p>
<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" rel="nofollow" href="https://cdn.substack.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8eabe9b-3cd4-438b-a344-8fa771d11dda_1600x454.jpeg"><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8eabe9b-3cd4-438b-a344-8fa771d11dda_1600x454.jpeg" data-attrs='{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/e8eabe9b-3cd4-438b-a344-8fa771d11dda_1600x454.jpeg","fullscreen":null,"imageSize":null,"height":413,"width":1456,"resizeWidth":null,"bytes":null,"alt":null,"title":null,"type":null,"href":null}' class="sizing-normal" alt="" srcset="https://cdn.substack.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8eabe9b-3cd4-438b-a344-8fa771d11dda_1600x454.jpeg 424w, https://cdn.substack.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8eabe9b-3cd4-438b-a344-8fa771d11dda_1600x454.jpeg 848w, https://cdn.substack.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8eabe9b-3cd4-438b-a344-8fa771d11dda_1600x454.jpeg 1272w, https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8eabe9b-3cd4-438b-a344-8fa771d11dda_1600x454.jpeg 1456w" sizes="100vw"></a><figcaption class="image-caption">An angkot minibus and standard full-size Transjakarta bus side by side. Minibuses are better suited for certain areas of Jakarta where winding, narrow streets make piloting a large Transjakarta bus nearly impossible. Source: Makeshift Mobility, Wikipedia</figcaption></figure></div>
<p>Other recent improvements include:</p>
<p>All these changes have led to higher quality service, better management, and most importantly, greater public trust in Jakarta’s mobility ecosystem as a whole. </p>
<h3><strong>Cycling</strong></h3>
<p>Jakarta’s government has also made serious strides towards expanding cycling infrastructure and integrating bikes into the broader transit system. </p>
<p><a href="https://www.bbc.com/future/bespoke/made-on-earth/the-great-bicycle-boom-of-2020.html" rel="">Many global cities experienced bike booms</a> during COVID, and while the pandemic shouldn’t be overlooked for its role in expediting cycling’s growth, Jakarta <a href="https://www.beritajakarta.id/en/read/31867/rss" rel="">was already adding many new bike lanes pre-pandemic</a>. But getting people to cycle in your city takes more than just building a few extra bike lanes. You also need to ensure that cycling is safe and convenient citywide. To that end, Jakarta has also focused on:</p>
<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" rel="nofollow" href="https://cdn.substack.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3aafde9-7adf-4bcb-9be7-97be25be9dc0_1600x900.jpeg"><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3aafde9-7adf-4bcb-9be7-97be25be9dc0_1600x900.jpeg" data-attrs='{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/d3aafde9-7adf-4bcb-9be7-97be25be9dc0_1600x900.jpeg","fullscreen":null,"imageSize":null,"height":819,"width":1456,"resizeWidth":null,"bytes":null,"alt":null,"title":null,"type":null,"href":null}' class="sizing-normal" alt="" srcset="https://cdn.substack.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3aafde9-7adf-4bcb-9be7-97be25be9dc0_1600x900.jpeg 424w, https://cdn.substack.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3aafde9-7adf-4bcb-9be7-97be25be9dc0_1600x900.jpeg 848w, https://cdn.substack.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3aafde9-7adf-4bcb-9be7-97be25be9dc0_1600x900.jpeg 1272w, https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3aafde9-7adf-4bcb-9be7-97be25be9dc0_1600x900.jpeg 1456w" sizes="100vw"></a><figcaption class="image-caption">President Director of MRT William Sabander (upper right) highlights recent bike-friendly upgrades to MRT interiors and stations during a presentation.</figcaption></figure></div>
<p></p>
<h3><strong>Why it works: putting it all together</strong></h3>
<p>As Jakarta shows, the formula for building a better transportation system doesn’t have to be complicated. If you want to reduce cars, you can’t just tell people to start driving less, you need to give them alternatives, like transit and cycling. <strong>And if you want people to actually adopt those alternatives, you need to make them as easy and enjoyable to use as possible.</strong> </p>
<p>Now <strong>just because it isn’t complicated doesn’t mean it’s easy to do. </strong>But Jakarta is proof that effective, dedicated government support and highly organized integration efforts can have an immediate impact on the quality of urban mobility. </p>
<h2><strong>Criticisms</strong></h2>
<p>So far today I’ve been pretty complimentary of Jakarta’s efforts (and for good reason, they’re great) but this wouldn’t be a fair assessment if we didn’t also look at a few criticisms/potential downsides as well.</p>
<h3>Jakarta v Greater Jakarta</h3>
<p>First off, when we talk about “urban mobility” it can be easy to forget that people are constantly traveling in and out of the city. So while improvements to Transjakarta and cycling infrastructure may help people <strong>within </strong>the city limits, commuters coming from the Greater Jakarta area may not have benefited as much from Jak Lingko and other intra-city initiatives. This results in many people still preferring (or having no other choice but) to drive, which, if you’ve been paying attention, is not ideal.</p>
<p>Now Jakarta <a href="https://en.wikipedia.org/wiki/KRL_Commuterline" rel="">does have a commuter rail system (KRL) </a>but…</p>
<ol><li><p>The system is currently not well-connected with most forms of transit in Jakarta (although there are future integrations with MRT planned). </p></li><li><p>The system is relatively outdated and<a href="https://www.arup.com/projects/greater-jakarta-commuter-rail-revitalisation" rel=""> struggles to handle the increasing rider volume of the Greater Jakarta region</a>. </p></li></ol>
<p>It’s also important to note that the commuter line is operated by a state-owned entity, Kereta Api, and thus it is not under Jakarta’s jurisdiction. However, what<strong> the city can do</strong> is make greater efforts to integrate MRT, Transjakarta, and cycling infrastructure in/around KRL stations. This would greatly increase the efficacy and attractiveness of KRL for people who live outside the central downtown area, helping decrease the number of cars on the road.</p>
<h3>Pollution persists</h3>
<p>The second criticism I have is that, despite local officials’ efforts to address congestion, Jakarta is <a href="https://www.channelnewsasia.com/asia/indonesia-jakarta-air-pollution-emissions-vehicles-factories-2285926#:~:text=According%20to%20air%20quality%20monitoring,quality%20was%20deemed%20%E2%80%9Chealthy%E2%80%9D." rel="">still an incredibly polluted city</a>. Now IMO this doesn’t take away from the city’s efforts to reduce driving. Instead, it is an indicator of the sheer scale of its traffic problems. To improve air quality further, there are definitely additional steps that Jak Lingko, Transjakarta, and the city as a whole could be taking. </p>
<p>For example, electrification of buses. While Transjakarta’s claim to fame is that it was the first-ever SE Asian BRT when it was built back in 2004, in the world of public transit, <strong>longevity often means outdated technology.</strong> Case in point, many of Jakarta’s buses are older, less-efficient diesel-fuel models (specifically Euro II and Euro III, for you bus nerds). <a href="https://en.antaranews.com/news/188437/transjakarta-commences-operational-trial-for-electric-bus" rel="">The system’s first two electric buses debuted in late 2021</a>, but the fleet currently stands at around <a href="https://youtu.be/6v75n2WniZ0?t=1061" rel="">~3,500 </a>buses in total. That makes the city’s goal of electrifying <a href="https://en.antaranews.com/news/217869/jakarta-targets-electrifying-50-pct-of-transjakartas-fleet-by-2025" rel="">half of its bus fleet by 2025</a> incredibly ambitious given its current progress.</p>
<h2><strong>Looking Forward</strong></h2>
<p>Despite these criticisms, the work Jakarta has done so far deserves immense credit and recognition. And the city isn’t resting on its laurels either. Planned future developments include:</p>
<p>Another thing we haven’t even touched on today are the huge advances that Jakarta’s <strong>private mobility players</strong> have made to reduce car dependency. Indonesia’s most famous tech unicorn GoJek (Now GoTo Group after a merger with fellow Indonesia unicorn Tokopedia) has poured significant money in the micromobility landscape in Indonesia, and specifically Jakarta. They’ve also attracted foreign investment from notable firms like <a href="https://www.prnewswire.com/news-releases/gogoro-drives-strong-momentum-in-indonesia-with-new-strategic-partnerships-that-establish-an-open-electric-mobility-and-battery-swapping-ecosystem-301467201.html" rel="">Taiwanese battery/moped company Gogoro</a>, and <a href="https://thediplomat.com/2022/02/gojek-and-foxconn-enter-indonesias-electric-vehicle-race/" rel="">leading chip maker Foxconn</a>. But that’s all for another newsletter. </p>
<h3><strong>Conclusion</strong></h3>
<p>Jakarta’s transformation from a congestion capital of the world to a leader in mobility policy and governance is something definitely worthy of global praise.</p>
<p>We already know that the problems of excess congestion and pollution won’t be solved by <a href="https://www.bloomberg.com/news/features/2021-09-28/why-widening-highways-doesn-t-bring-traffic-relief" rel="">widening roads</a> or making cars more efficient (EVs still cause traffic!). Instead, as Jakarta has shown, the goal is to<strong> reduce car trips altogether.</strong> And through its high-coverage bus network, ample cycling infrastructure, and strong top-down leadership, Jakarta is making it as easy as possible for residents and visitors to do just that.</p>
<p>Of course there’s still plenty of work to be done. But the fact that Jakarta has made this much progress to enhance its residents’ mobility choices, especially considering where it started from, is a sign of hope for all of us who want our cities to be greener, cleaner, and less congested.</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>

+ 13
- 0
cache/2022/a863c20d0cb9722df74219009e8365a3/index.md
File diff suppressed because it is too large
View File


+ 201
- 0
cache/2022/c7ebf32ee18c4f44c452f864729a21a8/index.html View File

@@ -0,0 +1,201 @@
<!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>The drone operators who halted Russian convoy headed for Kyiv (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://www.theguardian.com/world/2022/mar/28/the-drone-operators-who-halted-the-russian-armoured-vehicles-heading-for-kyiv">

<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>The drone operators who halted Russian convoy headed for Kyiv</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://www.theguardian.com/world/2022/mar/28/the-drone-operators-who-halted-the-russian-armoured-vehicles-heading-for-kyiv" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p class="dcr-1wj398p">One week into its invasion of Ukraine, Russia massed a <a href="https://www.theguardian.com/world/2022/mar/01/vast-russian-military-convoy-kyiv-siege-ukraine" data-link-name="in body link">40-mile mechanised column</a> in order to mount an overwhelming attack on Kyiv from the north.</p>
<p class="dcr-1wj398p">But the convoy of armoured vehicles and supply trucks ground to a halt within days, and the offensive failed, in significant part because of a series of night ambushes carried out by a team of 30 Ukrainian special forces and drone operators on quad bikes, according to a Ukrainian commander.</p>
<p id="sign-in-gate"><gu-island name="SignInGateSelector" props='{"format":{"display":0,"theme":0,"design":6},"contentType":"Article","sectionName":"world","tags":[{"id":"world/ukraine","type":"Keyword","title":"Ukraine"},{"id":"world/europe-news","type":"Keyword","title":"Europe"},{"id":"world/world","type":"Keyword","title":"World news"},{"id":"world/russia","type":"Keyword","title":"Russia"},{"id":"world/vladimir-putin","type":"Keyword","title":"Vladimir Putin"},{"id":"world/volodymyr-zelenskiy","type":"Keyword","title":"Volodymyr Zelenskiy"},{"id":"world/drones","type":"Keyword","title":"Drones (military)"},{"id":"technology/technology","type":"Keyword","title":"Technology"},{"id":"technology/crowdfunding","type":"Keyword","title":"Crowdfunding"},{"id":"type/article","type":"Type","title":"Article"},{"id":"tone/features","type":"Tone","title":"Features"},{"id":"profile/julianborger","type":"Contributor","title":"Julian Borger","bylineImageUrl":"https://i.guim.co.uk/img/uploads/2017/10/06/Julian-Borger,-R.png?width=300&amp;quality=85&amp;auto=format&amp;fit=max&amp;s=c3f61b78eab309dd315e59533afd01fa"},{"id":"publication/theguardian","type":"Publication","title":"The Guardian"},{"id":"theguardian/mainsection","type":"NewspaperBook","title":"Main section"},{"id":"theguardian/mainsection/uknews","type":"NewspaperBookSection","title":"UK news"},{"id":"tracking/commissioningdesk/us-foreign","type":"Tracking","title":"US Foreign"}],"isPaidContent":false,"isPreview":false,"host":"https://www.theguardian.com","pageId":"world/2022/mar/28/the-drone-operators-who-halted-the-russian-armoured-vehicles-heading-for-kyiv","idUrl":"https://profile.theguardian.com"}' clientonly="true"></gu-island></p>
<p class="dcr-1wj398p">The drone operators were drawn from an air reconnaissance unit, <a href="https://aerorozvidka.xyz/" data-link-name="in body link">Aerorozvidka</a>, which began eight years ago as a group of volunteer IT specialists and hobbyists designing their own machines and has evolved into an essential element in Ukraine’s successful David-and-Goliath resistance.</p>
<p class="dcr-1wj398p">However, while Ukraine’s western backers have supplied thousands of anti-tank and anti-aircraft missiles and other military equipment, Aerorozvidka has been forced to resort to <a href="https://www.facebook.com/aerorozvidka/" data-link-name="in body link">crowdfunding</a> and a network of personal contacts in order to keep going, by getting hold of components such as advanced modems and thermal imaging cameras, in the face of export controls that prohibit them being sent to Ukraine.</p>
<p class="dcr-1wj398p">The unit’s commander, Lt Col Yaroslav Honchar, gave an account of the ambush near the town of Ivankiv that helped stop the vast, lumbering Russian offensive in its tracks. He said the Ukrainian fighters on quad bikes were able to approach the advancing Russian column at night by riding through the forest on either side of the road leading south towards Kyiv from the direction of <a href="https://www.theguardian.com/world/2022/mar/09/chernobyl-power-supply-cut-completely-after-russian-seizure-warns-ukaine" data-link-name="in body link">Chernobyl</a>.</p>
<p class="dcr-1wj398p">The Ukrainian soldiers were equipped with night vision goggles, sniper rifles, remotely detonated mines, drones equipped with thermal imaging cameras and others capable of dropping small 1.5kg bombs.</p>
<p class="dcr-1wj398p">“This one little unit in the night destroyed two or three vehicles at the head of this convoy, and after that it was stuck. They stayed there two more nights, and [destroyed] many vehicles,” Honchar said.</p>
<figure id="7aea6ba3-5c70-4f75-977a-c047fd8d1a24" data-spacefinder-role="inline" data-spacefinder-type="model.dotcomrendering.pageElements.ImageBlockElement" class=" dcr-10khgmf"><figcaption class="dcr-w6u133"><span class="dcr-1usbar2"><svg viewbox="0 0 18 13"><path d="M18 3.5v8l-1.5 1.5h-15l-1.5-1.5v-8l1.5-1.5h3.5l2-2h4l2 2h3.5l1.5 1.5zm-9 7.5c1.9 0 3.5-1.6 3.5-3.5s-1.6-3.5-3.5-3.5-3.5 1.6-3.5 3.5 1.6 3.5 3.5 3.5z"></path></svg></span><span class="dcr-19x4pdv">A drone is assembled by the Aerorozvidka unit.</span> Photograph: Aerorozvidka</figcaption></figure>
<p class="dcr-1wj398p">The Russians broke the column into smaller units to try to make headway towards the Ukrainian capital, but the same assault team was able to mount an attack on its supply depot, he claimed, crippling the Russians’ capacity to advance.</p>
<p class="dcr-1wj398p">“The first echelon of the Russian force was stuck without heat, without oil, without bombs and without gas. And it all happened because of the work of 30 people,” Honchar said.</p>
<p class="dcr-1wj398p">The Aerorozvidka<em> </em>unit also claims to have helped defeat a Russian airborne attack on Hostomel airport, just north-west of Kyiv, in the first day of the war, using drones to locate, target and shell about 200 Russian paratroopers concealed at one end of the airfield.</p>
<p class="dcr-1wj398p">“That contributed largely to the fact that they could not use this airfield for further development of their attack,” Lt Taras, one of Honchar’s aides, said.</p>
<figure id="75a237d9-8fb1-4e47-9c48-dd3d64f0be4c" data-spacefinder-role="richLink" data-spacefinder-type="model.dotcomrendering.pageElements.RichLinkBlockElement" class=" dcr-1mfia18"><gu-island name="RichLinkComponent" deferuntil="idle" props='{"richLinkIndex":12,"element":{"_type":"model.dotcomrendering.pageElements.RichLinkBlockElement","url":"https://www.theguardian.com/world/2022/mar/10/drone-footage-russia-tanks-ambushed-ukraine-forces-kyiv-war","text":"Drone footage shows Ukrainian ambush on Russian tanks","prefix":"Related: ","role":"richLink","elementId":"75a237d9-8fb1-4e47-9c48-dd3d64f0be4c"},"ajaxUrl":"https://api.nextgen.guardianapps.co.uk","format":{"display":0,"theme":0,"design":6}}'></gu-island></figure>
<p class="dcr-1wj398p">Not all the details of these claims could be independently verified, but US defence officials have said that Ukrainian attacks contributed to the halting of the armoured column around Ivankiv. The huge amount of <a href="https://www.theguardian.com/world/2022/mar/10/drone-footage-russia-tanks-ambushed-ukraine-forces-kyiv-war" data-link-name="in body link">aerial combat footage</a> published by the Ukrainians underlines the importance of drones to their resistance.</p>
<p class="dcr-1wj398p">The unit was started by young university-educated Ukrainians who had been part of the 2014 <a href="https://www.theguardian.com/world/2014/feb/24/ukraine-task-uprising-political-victory" data-link-name="in body link">Maidan uprising</a> and volunteered to use their technical skills in the resistance against the first Russian invasion in Crimea and the Donbas region. Its founder, Volodymyr Kochetkov-Sukach, was an investment banker who was killed in action in 2015 in Donbas – a reminder of the high risks involved. The Russians can latch on to the drone’s electronic signature and quickly strike with mortars, so the Aerorozvidka teams have to launch and run.</p>
<p class="dcr-1wj398p">Honchar is an ex-soldier turned IT marketing consultant, who returned to the army after the first Russian invasion. Taras was a management consultant, who specialised in fundraising for the unit and only joined full-time as a combatant in February.</p>
<p class="dcr-1wj398p">In its early days, the unit used commercial surveillance drones, but its team of engineers, software designers and drone enthusiasts later developed their own designs. </p>
<p class="dcr-1wj398p">They built a range of surveillance drones, as well as large 1.5-metre eight-rotor machines capable of dropping bombs and rocket-propelled anti-tank grenades, and created a system called Delta, a network of sensors along the frontlines that fed into a digital map so commanders could see enemy movements as they happened. It now uses the Starlink satellite system, supplied by Elon Musk, to feed live data to Ukrainian artillery units, allowing them to zero in on Russian targets.</p>
<p class="dcr-1wj398p">The unit was disbanded in 2019 by the then defence minister, but it was hastily revived in October last year as the Russian invasion threat loomed.</p>
<p class="dcr-1wj398p">The ability to maintain an aerial view of Russian movements has been critical to the success of Ukraine’s guerrilla-style tactics. But Aerorozvidka’s efforts to expand, and to replace lost equipment, have been hindered by a limited supply of drones and components, and efforts to secure them through defence ministry procurement have produced little, partly because they are a recent addition to the armed forces and still considered outsiders.</p>
<p class="dcr-1wj398p">Furthermore, some of the advanced modems and thermal-imaging cameras made in the US and Canada are subject to export controls, so they have resorted to crowdfunding and asking a global network of friends and supporters to find them on eBay or other websites.</p>
<p class="dcr-1wj398p">Marina Borozna, who was an economics student at university with Taras, is exploring ways of buying what the unit needs and finding routes to get the supplies across the border.</p>
<p class="dcr-1wj398p">“I know there are people who want to help them fight, people who want to do a bit more than the humanitarian aid,” Borozna said. “If you want to address the root cause of this human suffering, you’ve got to defeat the Russian invasion. Aerorozvidka makes a huge difference and they need our support.”</p>
<p class="dcr-1wj398p">Her partner, Klaus Hentrich, a molecular biologist in Cambridge, is also helping the effort, drawing on his experience as a conscript in the German army.</p>
<p class="dcr-1wj398p">“I was in an artillery reconnaissance unit myself, so I immediately realised the outsized impact that Aerorozvidka has. They effectively give eyes to their artillery,” Hentrich said. “Where we can make a difference is to rally international support, be it financial contributions, help to get harder-to-find technical components or donations of common civilian drones.”</p>
<figure id="f1c0e0a6-e391-44da-aedb-4ce32872e73b" data-spacefinder-role="richLink" data-spacefinder-type="model.dotcomrendering.pageElements.RichLinkBlockElement" class=" dcr-1mfia18"><gu-island name="RichLinkComponent" deferuntil="idle" props='{"richLinkIndex":25,"element":{"_type":"model.dotcomrendering.pageElements.RichLinkBlockElement","url":"https://www.theguardian.com/world/2022/mar/23/military-supplies-depleted-on-both-sides-but-russia-retains-advantage","text":"Military supplies depleted on both sides but Russia retains advantage","prefix":"Related: ","role":"richLink","elementId":"f1c0e0a6-e391-44da-aedb-4ce32872e73b"},"ajaxUrl":"https://api.nextgen.guardianapps.co.uk","format":{"display":0,"theme":0,"design":6}}'></gu-island></figure>
<p class="dcr-1wj398p">The unit is also looking at ways to overcome Russian jamming, part of the <a href="https://www.theguardian.com/world/2022/feb/24/russia-unleashed-data-wiper-virus-on-ukraine-say-cyber-experts" data-link-name="in body link">electronic warfare being waged</a> in Ukraine in parallel to the bombs, shells and missiles. At present, Aerorozvidka typically waits for the Russians turn off their jamming equipment to launch their own drones, and then it sends up its machines at the same time. The unit then concentrates its firepower on the electronic warfare vehicles.</p>
<p class="dcr-1wj398p">Honchar describes these technological battles, and Aerorozvidka’s<em> </em>way of fighting, as the future of warfare, in which swarms of small teams networked together by mutual trust and advanced communications can overwhelm a bigger and more heavily armed adversary.</p>
<p class="dcr-1wj398p">“We are like a hive of bees,” he said. “One bee is nothing, but if you are faced with a thousand, it can defeat a big force. We are like bees, but we work at night.”</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>

+ 8
- 0
cache/2022/c7ebf32ee18c4f44c452f864729a21a8/index.md View File

@@ -0,0 +1,8 @@
title: The drone operators who halted Russian convoy headed for Kyiv
url: https://www.theguardian.com/world/2022/mar/28/the-drone-operators-who-halted-the-russian-armoured-vehicles-heading-for-kyiv
hash_url: c7ebf32ee18c4f44c452f864729a21a8

<p class="dcr-1wj398p">One week into its invasion of Ukraine, Russia massed a <a href="https://www.theguardian.com/world/2022/mar/01/vast-russian-military-convoy-kyiv-siege-ukraine" data-link-name="in body link">40-mile mechanised column</a> in order to mount an overwhelming attack on Kyiv from the north.</p><p class="dcr-1wj398p">But the convoy of armoured vehicles and supply trucks ground to a halt within days, and the offensive failed, in significant part because of a series of night ambushes carried out by a team of 30 Ukrainian special forces and drone operators on quad bikes, according to a Ukrainian commander.</p><p id="sign-in-gate"><gu-island name="SignInGateSelector" props='{"format":{"display":0,"theme":0,"design":6},"contentType":"Article","sectionName":"world","tags":[{"id":"world/ukraine","type":"Keyword","title":"Ukraine"},{"id":"world/europe-news","type":"Keyword","title":"Europe"},{"id":"world/world","type":"Keyword","title":"World news"},{"id":"world/russia","type":"Keyword","title":"Russia"},{"id":"world/vladimir-putin","type":"Keyword","title":"Vladimir Putin"},{"id":"world/volodymyr-zelenskiy","type":"Keyword","title":"Volodymyr Zelenskiy"},{"id":"world/drones","type":"Keyword","title":"Drones (military)"},{"id":"technology/technology","type":"Keyword","title":"Technology"},{"id":"technology/crowdfunding","type":"Keyword","title":"Crowdfunding"},{"id":"type/article","type":"Type","title":"Article"},{"id":"tone/features","type":"Tone","title":"Features"},{"id":"profile/julianborger","type":"Contributor","title":"Julian Borger","bylineImageUrl":"https://i.guim.co.uk/img/uploads/2017/10/06/Julian-Borger,-R.png?width=300&amp;quality=85&amp;auto=format&amp;fit=max&amp;s=c3f61b78eab309dd315e59533afd01fa"},{"id":"publication/theguardian","type":"Publication","title":"The Guardian"},{"id":"theguardian/mainsection","type":"NewspaperBook","title":"Main section"},{"id":"theguardian/mainsection/uknews","type":"NewspaperBookSection","title":"UK news"},{"id":"tracking/commissioningdesk/us-foreign","type":"Tracking","title":"US Foreign"}],"isPaidContent":false,"isPreview":false,"host":"https://www.theguardian.com","pageId":"world/2022/mar/28/the-drone-operators-who-halted-the-russian-armoured-vehicles-heading-for-kyiv","idUrl":"https://profile.theguardian.com"}' clientonly="true"></gu-island></p><p class="dcr-1wj398p">The drone operators were drawn from an air reconnaissance unit, <a href="https://aerorozvidka.xyz/" data-link-name="in body link">Aerorozvidka</a>, which began eight years ago as a group of volunteer IT specialists and hobbyists designing their own machines and has evolved into an essential element in Ukraine’s successful David-and-Goliath resistance.</p><p class="dcr-1wj398p">However, while Ukraine’s western backers have supplied thousands of anti-tank and anti-aircraft missiles and other military equipment, Aerorozvidka has been forced to resort to <a href="https://www.facebook.com/aerorozvidka/" data-link-name="in body link">crowdfunding</a> and a network of personal contacts in order to keep going, by getting hold of components such as advanced modems and thermal imaging cameras, in the face of export controls that prohibit them being sent to Ukraine.</p><p class="dcr-1wj398p">The unit’s commander, Lt Col Yaroslav Honchar, gave an account of the ambush near the town of Ivankiv that helped stop the vast, lumbering Russian offensive in its tracks. He said the Ukrainian fighters on quad bikes were able to approach the advancing Russian column at night by riding through the forest on either side of the road leading south towards Kyiv from the direction of <a href="https://www.theguardian.com/world/2022/mar/09/chernobyl-power-supply-cut-completely-after-russian-seizure-warns-ukaine" data-link-name="in body link">Chernobyl</a>.</p><p class="dcr-1wj398p">The Ukrainian soldiers were equipped with night vision goggles, sniper rifles, remotely detonated mines, drones equipped with thermal imaging cameras and others capable of dropping small 1.5kg bombs.</p><p class="dcr-1wj398p">“This one little unit in the night destroyed two or three vehicles at the head of this convoy, and after that it was stuck. They stayed there two more nights, and [destroyed] many vehicles,” Honchar said.</p>
<figure id="7aea6ba3-5c70-4f75-977a-c047fd8d1a24" data-spacefinder-role="inline" data-spacefinder-type="model.dotcomrendering.pageElements.ImageBlockElement" class=" dcr-10khgmf"><figcaption class="dcr-w6u133"><span class="dcr-1usbar2"><svg viewbox="0 0 18 13"><path d="M18 3.5v8l-1.5 1.5h-15l-1.5-1.5v-8l1.5-1.5h3.5l2-2h4l2 2h3.5l1.5 1.5zm-9 7.5c1.9 0 3.5-1.6 3.5-3.5s-1.6-3.5-3.5-3.5-3.5 1.6-3.5 3.5 1.6 3.5 3.5 3.5z"></path></svg></span><span class="dcr-19x4pdv">A drone is assembled by the Aerorozvidka unit.</span> Photograph: Aerorozvidka</figcaption></figure><p class="dcr-1wj398p">The Russians broke the column into smaller units to try to make headway towards the Ukrainian capital, but the same assault team was able to mount an attack on its supply depot, he claimed, crippling the Russians’ capacity to advance.</p><p class="dcr-1wj398p">“The first echelon of the Russian force was stuck without heat, without oil, without bombs and without gas. And it all happened because of the work of 30 people,” Honchar said.</p><p class="dcr-1wj398p">The Aerorozvidka<em> </em>unit also claims to have helped defeat a Russian airborne attack on Hostomel airport, just north-west of Kyiv, in the first day of the war, using drones to locate, target and shell about 200 Russian paratroopers concealed at one end of the airfield.</p><p class="dcr-1wj398p">“That contributed largely to the fact that they could not use this airfield for further development of their attack,” Lt Taras, one of Honchar’s aides, said.</p>
<figure id="75a237d9-8fb1-4e47-9c48-dd3d64f0be4c" data-spacefinder-role="richLink" data-spacefinder-type="model.dotcomrendering.pageElements.RichLinkBlockElement" class=" dcr-1mfia18"><gu-island name="RichLinkComponent" deferuntil="idle" props='{"richLinkIndex":12,"element":{"_type":"model.dotcomrendering.pageElements.RichLinkBlockElement","url":"https://www.theguardian.com/world/2022/mar/10/drone-footage-russia-tanks-ambushed-ukraine-forces-kyiv-war","text":"Drone footage shows Ukrainian ambush on Russian tanks","prefix":"Related: ","role":"richLink","elementId":"75a237d9-8fb1-4e47-9c48-dd3d64f0be4c"},"ajaxUrl":"https://api.nextgen.guardianapps.co.uk","format":{"display":0,"theme":0,"design":6}}'></gu-island></figure><p class="dcr-1wj398p">Not all the details of these claims could be independently verified, but US defence officials have said that Ukrainian attacks contributed to the halting of the armoured column around Ivankiv. The huge amount of <a href="https://www.theguardian.com/world/2022/mar/10/drone-footage-russia-tanks-ambushed-ukraine-forces-kyiv-war" data-link-name="in body link">aerial combat footage</a> published by the Ukrainians underlines the importance of drones to their resistance.</p><p class="dcr-1wj398p">The unit was started by young university-educated Ukrainians who had been part of the 2014 <a href="https://www.theguardian.com/world/2014/feb/24/ukraine-task-uprising-political-victory" data-link-name="in body link">Maidan uprising</a> and volunteered to use their technical skills in the resistance against the first Russian invasion in Crimea and the Donbas region. Its founder, Volodymyr Kochetkov-Sukach, was an investment banker who was killed in action in 2015 in Donbas – a reminder of the high risks involved. The Russians can latch on to the drone’s electronic signature and quickly strike with mortars, so the Aerorozvidka teams have to launch and run.</p><p class="dcr-1wj398p">Honchar is an ex-soldier turned IT marketing consultant, who returned to the army after the first Russian invasion. Taras was a management consultant, who specialised in fundraising for the unit and only joined full-time as a combatant in February.</p><p class="dcr-1wj398p">In its early days, the unit used commercial surveillance drones, but its team of engineers, software designers and drone enthusiasts later developed their own designs. </p><p class="dcr-1wj398p">They built a range of surveillance drones, as well as large 1.5-metre eight-rotor machines capable of dropping bombs and rocket-propelled anti-tank grenades, and created a system called Delta, a network of sensors along the frontlines that fed into a digital map so commanders could see enemy movements as they happened. It now uses the Starlink satellite system, supplied by Elon Musk, to feed live data to Ukrainian artillery units, allowing them to zero in on Russian targets.</p><p class="dcr-1wj398p">The unit was disbanded in 2019 by the then defence minister, but it was hastily revived in October last year as the Russian invasion threat loomed.</p><p class="dcr-1wj398p">The ability to maintain an aerial view of Russian movements has been critical to the success of Ukraine’s guerrilla-style tactics. But Aerorozvidka’s efforts to expand, and to replace lost equipment, have been hindered by a limited supply of drones and components, and efforts to secure them through defence ministry procurement have produced little, partly because they are a recent addition to the armed forces and still considered outsiders.</p><p class="dcr-1wj398p">Furthermore, some of the advanced modems and thermal-imaging cameras made in the US and Canada are subject to export controls, so they have resorted to crowdfunding and asking a global network of friends and supporters to find them on eBay or other websites.</p><p class="dcr-1wj398p">Marina Borozna, who was an economics student at university with Taras, is exploring ways of buying what the unit needs and finding routes to get the supplies across the border.</p><p class="dcr-1wj398p">“I know there are people who want to help them fight, people who want to do a bit more than the humanitarian aid,” Borozna said. “If you want to address the root cause of this human suffering, you’ve got to defeat the Russian invasion. Aerorozvidka makes a huge difference and they need our support.”</p><p class="dcr-1wj398p">Her partner, Klaus Hentrich, a molecular biologist in Cambridge, is also helping the effort, drawing on his experience as a conscript in the German army.</p><p class="dcr-1wj398p">“I was in an artillery reconnaissance unit myself, so I immediately realised the outsized impact that Aerorozvidka has. They effectively give eyes to their artillery,” Hentrich said. “Where we can make a difference is to rally international support, be it financial contributions, help to get harder-to-find technical components or donations of common civilian drones.”</p>
<figure id="f1c0e0a6-e391-44da-aedb-4ce32872e73b" data-spacefinder-role="richLink" data-spacefinder-type="model.dotcomrendering.pageElements.RichLinkBlockElement" class=" dcr-1mfia18"><gu-island name="RichLinkComponent" deferuntil="idle" props='{"richLinkIndex":25,"element":{"_type":"model.dotcomrendering.pageElements.RichLinkBlockElement","url":"https://www.theguardian.com/world/2022/mar/23/military-supplies-depleted-on-both-sides-but-russia-retains-advantage","text":"Military supplies depleted on both sides but Russia retains advantage","prefix":"Related: ","role":"richLink","elementId":"f1c0e0a6-e391-44da-aedb-4ce32872e73b"},"ajaxUrl":"https://api.nextgen.guardianapps.co.uk","format":{"display":0,"theme":0,"design":6}}'></gu-island></figure><p class="dcr-1wj398p">The unit is also looking at ways to overcome Russian jamming, part of the <a href="https://www.theguardian.com/world/2022/feb/24/russia-unleashed-data-wiper-virus-on-ukraine-say-cyber-experts" data-link-name="in body link">electronic warfare being waged</a> in Ukraine in parallel to the bombs, shells and missiles. At present, Aerorozvidka typically waits for the Russians turn off their jamming equipment to launch their own drones, and then it sends up its machines at the same time. The unit then concentrates its firepower on the electronic warfare vehicles.</p><p class="dcr-1wj398p">Honchar describes these technological battles, and Aerorozvidka’s<em> </em>way of fighting, as the future of warfare, in which swarms of small teams networked together by mutual trust and advanced communications can overwhelm a bigger and more heavily armed adversary.</p><p class="dcr-1wj398p">“We are like a hive of bees,” he said. “One bee is nothing, but if you are faced with a thousand, it can defeat a big force. We are like bees, but we work at night.”</p>

+ 182
- 0
cache/2022/cf85372fcb8da232d3fb8d95a88bc8fe/index.html View File

@@ -0,0 +1,182 @@
<!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>Stop making the Ukraine war about you (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://www.dazeddigital.com/politics/article/55563/1/stop-making-the-ukraine-war-about-you">

<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>Stop making the Ukraine war about you</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://www.dazeddigital.com/politics/article/55563/1/stop-making-the-ukraine-war-about-you" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<h2 class="summary">You’re not suffering from ‘vicarious trauma’, you’re tweeting in your <span class="nowrap">living room</span></h2>
<div class="embed-content" data-embed-type="raw-html"><p><span>It’s a testament to the dominance of therapy speak in our culture that, confronted with the news of a conflict taking place in a different country, the reaction of many has been to frame this tragedy in relation to our own mental health, as if the most important matter at hand is for people in the west to avoid experiencing anxiety or secondhand distress. One article published on the <em>Huffington Post</em> offered guidance on how to avoid getting “</span><a href="https://twitter.com/thor_benson/status/1496986150849888256?s=20&amp;t=rX_YQEzThP7I9V4nMuOnpA"><span>vicarious trauma</span></a><span>” from hearing about what’s happening. This is a real concept, but it’s typically something experienced by professionals who work closely with traumatised people. Similarly, people who have to view traumatising material at work, such as Facebook moderators, have been shown to develop PTSD. So clearly, exposure to upsetting material can be traumatising in itself. However, for the most part this isn’t what’s happening when people are reading tweets about Ukraine.</span></p>
<p><span>Some of the advice about this – in the form of tweets, articles and</span><a href="https://www.instagram.com/p/CaZ0cO-l1lQ/?utm_medium=copy_link"><span> Instagram graphics</span></a><span> about the importance of logging off and practising <a href="https://twitter.com/NPR/status/1497291026347835394?s=20&amp;t=QL-3broa5k3O06HODdL5rA">self-care</a> – takes on a strangely universalising air. No doubt there are people in the UK who genuinely have post-conflict PTSD or serious anxiety disorders and need to take care to avoid triggers, but a lot of this stuff implies that this is true of everyone, that every single person reading is a traumatised former refugee or war reporter. What’s more likely is that most people, upon viewing upsetting footage of events taking place in a different country, feel kind of sad and worried. It’s fine to want to avoid that, but I don’t think it’s an especially laudable impulse. If you’re not Ukrainian, don’t have loved ones in Ukraine, and/or don’t have PTSD, then prioritising your own entitlement not to feel troubled about what’s happening there just strikes me as self-indulgent. </span></p></div>
<div class="embed-content" data-embed-type="raw-html"><p><span>Social media isn’t necessarily the source of this tendency, but it has accelerated the impulse for people to be obsessed with their own subjectivity; unable to process global events outside of the prism of their own emotional reaction and the relatively minor ways they are affected. This isn’t a good way of relating to the world. More than anything, all this talk of “vicarious trauma”, “</span><a href="https://twitter.com/thor_benson/status/1496986150849888256?s=20&amp;t=rX_YQEzThP7I9V4nMuOnpA"><span>living through historic events</span></a><span>” and “the importance of stepping away” reminds me of the concept of “allyship fatigue” which emerged around the George Floyd protests in 2020, when white people began expressing their exhaustion with being forced to care about the oppression of Black people. Rightly, this idea became widely </span><a href="https://twitter.com/KindaHagi/status/1363353920823779332?s=20&amp;t=qns99KAdMTS96vAWaamPYw"><span>mocked</span></a><span> and denounced; </span><a href="https://www.google.com/search?client=safari&amp;rls=en&amp;q=insult+to+Black+folks+who+never+get+to+rest&amp;ie=UTF-8&amp;oe=UTF-8&amp;safari_group=7"><span>described</span></a><span> by writer Sherronda J Brown as an “insult to Black folks who never get to rest.” </span></p>
<p><span>But it seems like we haven’t learned our lesson, because, while the racial dynamic might be different, the reaction of large swathes of people to recent events is striking a similar note. The invasion of Ukraine is not something that is happening </span><em><span>to </span></em><span>us, and I don’t think claiming to be traumatised secondhand by it is suggestive of real empathy. It is, in fact, a corrosive impulse to make yourself the victim of a tragedy which is happening to other people, to hear about their suffering and prioritise your own self-care. It doesn’t strike me as empathetic to announce yourself uniquely entitled to avoid witnessing the suffering of other people, and how your heightened emotional intelligence makes doing so a real bummer – for you. It’s fine to moderate your news intake, but doing so quietly would be less crass. It’s also reasonable to be anxious about what might happen next, but we should bear in mind that, as of now, this is first and foremost something which is happening to people in Ukraine. We are not being bombed or driven out of our homes.</span></p></div>
<div class="embed-content" data-embed-type="raw-html"><p><span>There’s also a racial disparity at play in the expectation that everyone in the UK needs trauma counselling over what is happening in Ukraine. I don’t remember these kinds of articles coming out last May when bombs were raining on Gaza. I don’t think there was any suggestion then that we would be at risk of  ‘vicarious trauma’ from witnessing the suffering of Palestinians, and British Palestinians themselves were certainly offered no such babying, coddling reassurances. Some violent military occupations are deemed more “vicariously traumatic” than others, and a key factor here, along with race, is the UK’s own foreign policy. The same holds true for the war in Yemen, where Saudi Arabia has been responsible for the deaths of tens of thousands of civilians with the support of Britain. There has been no sanctions or boycotts; relatively little coverage and certainly far less suggestion that we as onlookers might be unduly disturbed to hear about these atrocities.</span></p>
<p><span>If you live in the UK and have loved ones in any conflict-affected area, then to be troubled by what’s happening in the news is simply a fact of life. It’s rarely the case that you are offered the same sympathy being afforded now to random people who spend too much time on Twitter. While presenting itself as progressive, the expectation that the invasion of Ukraine ought to be uniquely harrowing for </span><em><span>us</span></em><span> to hear about reproduces the same hierarchy of suffering as the commentators arguing that the situation is particularly bad because it’s happening to “</span><a href="https://twitter.com/thor_benson/status/1496986150849888256?s=20&amp;t=rX_YQEzThP7I9V4nMuOnpA"><span>civilised</span></a><span>” Europeans, people with “</span><a href="https://www.indiatoday.in/world/russia-ukraine-war/story/russia-ukraine-war-news-latest-racism-row-white-skin-blue-eyes-killed-1918857-2022-02-28"><span>blonde hair and blue eyes</span></a><span>”; people who “</span><a href="https://twitter.com/AymanM/status/1497946988117168137?s=20&amp;t=E1tRJV3k8dnykGslxtdHcQ"><span>watch Netflix and have Instagram accounts</span></a><span>.” </span></p>
<p><span>It’s understandable that the proximity of Ukraine, and the fact that the aggressors are not our allies, might make this more stressful for people in the UK. Given that Putin has put his </span><a href="https://www.nytimes.com/2022/02/27/us/politics/putin-nuclear-alert-biden-deescalation.html?campaign_id=51&amp;emc=edit_mbe_20220228&amp;instance_id=54442&amp;nl=morning-briefing%3A-europe-edition&amp;regi_id=93690126&amp;segment_id=84137&amp;te=1&amp;user_id=1e47cf13b3fcc4803477e4d2602753e1"><span>nuclear forces</span></a><span> on alert, it doesn’t seem unwarranted to be worried about potential escalation. But the World War 3 framing, whether in the form of TikTok meme accounts or in earnest, is a way of universalising a crisis that, as of now, is affecting other people. It’s a way for us to stake a claim on the situation, and turn ourselves into the protagonist. “</span><span>It signals a total failure to grasp the basic point of what’s happening,” Mark O’Connell, author of </span><a href="https://www.penguinrandomhouse.com/books/558414/notes-from-an-apocalypse-by-mark-oconnell/"><em><span>Notes from an Apocalypse: A Personal Journey to the End of the World</span></em></a><em><span>,</span></em><span> tells Dazed. “To talk about this as being the beginning of some notional ‘World War Three’ is to overlook the fact that this is about Russia invading the very specific nation of Ukraine, and the fact that Ukrainians, specifically, are going to be robbed of their country and their lives. It’s a way of eliding that specificity, and making it about all of us, and, by implication, me personally. In that sense, it’s the opposite of empathy. And it reduces a very specific political and humanitarian crisis to a sense of ambient unease or, worse, a kind of childish fantasy of living in the end times. It’s not about us; it’s about Ukrainians.”</span></p></div>
<div class="embed-content" data-embed-type="raw-html"><p><span>While the self-care discourse is deadly earnest, references to World War 3 tend to take the form of glib humour, usually some variation of “first a literal pandemic, and now a World War 3!” Sometimes, you get the impression that people are approaching the idea of nuclear annihilation with a kind of relish. There’s also been a spate of jokes about why, in light of the world ending, we shouldn’t have to go to work. While I am a deeply lazy person who thinks that slacking off is a moral good, it’s enough to make me want to scream: “shut your gob and do your job!” This whiny, babyish self-victimisation in the face of other people’s suffering is grotesque: if you want to skive off work, just do it, but don’t dress it up as a form of solidarity. Wry, world-weary apocalypticism has become the most viscerally annoying genre of internet humour. Apart from anything, it’s just boring, trite, and unfunny to be tweeting about “living through the literal end of days”, when you’re sitting cosy in your flat, ordering Deliveroo and watching Netflix. It’s an expression of real anxieties, I think, but there’s something smug about it. It’s gallows humour for people who aren’t really on the gallows.</span></p>
<p><span>“</span><span>In one sense, this register is just annoying because it’s tacky and cliched,” says Mark. “But again, it’s annoying because it just seems so lazily narcissistic. One thing I will say about the idea of the apocalypse is that it’s usually at least as much a fantasy as it is fear. As often as not, it’s a narcissistic fantasy about being a witness, and subject, of the end of everything. And when it’s invoked, as it so often is, in this world-weary, ironic register it’s a way of seeming to be maximally serious – because what could be more serious than the end of the world? – while being about as frivolous as it’s possible to be. </span><span>If people really did believe a nuclear exchange was imminent, they wouldn’t be acting all ironic and world-weary about it. Obviously, I can’t speak for the people of Ukraine, a country I have visited only briefly and know not enough about, but I am guessing world-weary irony is not the dominant affective tone on the streets of Kyiv, or in the crowded metro stations beneath them.”</span></p>
<p><span>The situation unfolding in Ukraine is stressful, anxiety-inducing and unpleasant. To feel upset by it is entirely unremarkable. But there has to be a way of showing solidarity with the Ukrainian people that doesn’t involve centring our own emotional reactions, our own terrible jokes.</span></p></div>
</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>

+ 15
- 0
cache/2022/cf85372fcb8da232d3fb8d95a88bc8fe/index.md View File

@@ -0,0 +1,15 @@
title: Stop making the Ukraine war about you
url: https://www.dazeddigital.com/politics/article/55563/1/stop-making-the-ukraine-war-about-you
hash_url: cf85372fcb8da232d3fb8d95a88bc8fe

<h2 class="summary">You’re not suffering from ‘vicarious trauma’, you’re tweeting in your <span class="nowrap">living room</span></h2>
<div class="embed-content" data-embed-type="raw-html"><p><span>It’s a testament to the dominance of therapy speak in our culture that, confronted with the news of a conflict taking place in a different country, the reaction of many has been to frame this tragedy in relation to our own mental health, as if the most important matter at hand is for people in the west to avoid experiencing anxiety or secondhand distress. One article published on the <em>Huffington Post</em> offered guidance on how to avoid getting “</span><a href="https://twitter.com/thor_benson/status/1496986150849888256?s=20&amp;t=rX_YQEzThP7I9V4nMuOnpA"><span>vicarious trauma</span></a><span>” from hearing about what’s happening. This is a real concept, but it’s typically something experienced by professionals who work closely with traumatised people. Similarly, people who have to view traumatising material at work, such as Facebook moderators, have been shown to develop PTSD. So clearly, exposure to upsetting material can be traumatising in itself. However, for the most part this isn’t what’s happening when people are reading tweets about Ukraine.</span></p>
<p><span>Some of the advice about this – in the form of tweets, articles and</span><a href="https://www.instagram.com/p/CaZ0cO-l1lQ/?utm_medium=copy_link"><span> Instagram graphics</span></a><span> about the importance of logging off and practising <a href="https://twitter.com/NPR/status/1497291026347835394?s=20&amp;t=QL-3broa5k3O06HODdL5rA">self-care</a> – takes on a strangely universalising air. No doubt there are people in the UK who genuinely have post-conflict PTSD or serious anxiety disorders and need to take care to avoid triggers, but a lot of this stuff implies that this is true of everyone, that every single person reading is a traumatised former refugee or war reporter. What’s more likely is that most people, upon viewing upsetting footage of events taking place in a different country, feel kind of sad and worried. It’s fine to want to avoid that, but I don’t think it’s an especially laudable impulse. If you’re not Ukrainian, don’t have loved ones in Ukraine, and/or don’t have PTSD, then prioritising your own entitlement not to feel troubled about what’s happening there just strikes me as self-indulgent. </span></p></div>
<div class="embed-content" data-embed-type="raw-html"><p><span>Social media isn’t necessarily the source of this tendency, but it has accelerated the impulse for people to be obsessed with their own subjectivity; unable to process global events outside of the prism of their own emotional reaction and the relatively minor ways they are affected. This isn’t a good way of relating to the world. More than anything, all this talk of “vicarious trauma”, “</span><a href="https://twitter.com/thor_benson/status/1496986150849888256?s=20&amp;t=rX_YQEzThP7I9V4nMuOnpA"><span>living through historic events</span></a><span>” and “the importance of stepping away” reminds me of the concept of “allyship fatigue” which emerged around the George Floyd protests in 2020, when white people began expressing their exhaustion with being forced to care about the oppression of Black people. Rightly, this idea became widely </span><a href="https://twitter.com/KindaHagi/status/1363353920823779332?s=20&amp;t=qns99KAdMTS96vAWaamPYw"><span>mocked</span></a><span> and denounced; </span><a href="https://www.google.com/search?client=safari&amp;rls=en&amp;q=insult+to+Black+folks+who+never+get+to+rest&amp;ie=UTF-8&amp;oe=UTF-8&amp;safari_group=7"><span>described</span></a><span> by writer Sherronda J Brown as an “insult to Black folks who never get to rest.” </span></p>
<p><span>But it seems like we haven’t learned our lesson, because, while the racial dynamic might be different, the reaction of large swathes of people to recent events is striking a similar note. The invasion of Ukraine is not something that is happening </span><em><span>to </span></em><span>us, and I don’t think claiming to be traumatised secondhand by it is suggestive of real empathy. It is, in fact, a corrosive impulse to make yourself the victim of a tragedy which is happening to other people, to hear about their suffering and prioritise your own self-care. It doesn’t strike me as empathetic to announce yourself uniquely entitled to avoid witnessing the suffering of other people, and how your heightened emotional intelligence makes doing so a real bummer – for you. It’s fine to moderate your news intake, but doing so quietly would be less crass. It’s also reasonable to be anxious about what might happen next, but we should bear in mind that, as of now, this is first and foremost something which is happening to people in Ukraine. We are not being bombed or driven out of our homes.</span></p></div>
<div class="embed-content" data-embed-type="raw-html"><p><span>There’s also a racial disparity at play in the expectation that everyone in the UK needs trauma counselling over what is happening in Ukraine. I don’t remember these kinds of articles coming out last May when bombs were raining on Gaza. I don’t think there was any suggestion then that we would be at risk of  ‘vicarious trauma’ from witnessing the suffering of Palestinians, and British Palestinians themselves were certainly offered no such babying, coddling reassurances. Some violent military occupations are deemed more “vicariously traumatic” than others, and a key factor here, along with race, is the UK’s own foreign policy. The same holds true for the war in Yemen, where Saudi Arabia has been responsible for the deaths of tens of thousands of civilians with the support of Britain. There has been no sanctions or boycotts; relatively little coverage and certainly far less suggestion that we as onlookers might be unduly disturbed to hear about these atrocities.</span></p>
<p><span>If you live in the UK and have loved ones in any conflict-affected area, then to be troubled by what’s happening in the news is simply a fact of life. It’s rarely the case that you are offered the same sympathy being afforded now to random people who spend too much time on Twitter. While presenting itself as progressive, the expectation that the invasion of Ukraine ought to be uniquely harrowing for </span><em><span>us</span></em><span> to hear about reproduces the same hierarchy of suffering as the commentators arguing that the situation is particularly bad because it’s happening to “</span><a href="https://twitter.com/thor_benson/status/1496986150849888256?s=20&amp;t=rX_YQEzThP7I9V4nMuOnpA"><span>civilised</span></a><span>” Europeans, people with “</span><a href="https://www.indiatoday.in/world/russia-ukraine-war/story/russia-ukraine-war-news-latest-racism-row-white-skin-blue-eyes-killed-1918857-2022-02-28"><span>blonde hair and blue eyes</span></a><span>”; people who “</span><a href="https://twitter.com/AymanM/status/1497946988117168137?s=20&amp;t=E1tRJV3k8dnykGslxtdHcQ"><span>watch Netflix and have Instagram accounts</span></a><span>.” </span></p>
<p><span>It’s understandable that the proximity of Ukraine, and the fact that the aggressors are not our allies, might make this more stressful for people in the UK. Given that Putin has put his </span><a href="https://www.nytimes.com/2022/02/27/us/politics/putin-nuclear-alert-biden-deescalation.html?campaign_id=51&amp;emc=edit_mbe_20220228&amp;instance_id=54442&amp;nl=morning-briefing%3A-europe-edition&amp;regi_id=93690126&amp;segment_id=84137&amp;te=1&amp;user_id=1e47cf13b3fcc4803477e4d2602753e1"><span>nuclear forces</span></a><span> on alert, it doesn’t seem unwarranted to be worried about potential escalation. But the World War 3 framing, whether in the form of TikTok meme accounts or in earnest, is a way of universalising a crisis that, as of now, is affecting other people. It’s a way for us to stake a claim on the situation, and turn ourselves into the protagonist. “</span><span>It signals a total failure to grasp the basic point of what’s happening,” Mark O’Connell, author of </span><a href="https://www.penguinrandomhouse.com/books/558414/notes-from-an-apocalypse-by-mark-oconnell/"><em><span>Notes from an Apocalypse: A Personal Journey to the End of the World</span></em></a><em><span>,</span></em><span> tells Dazed. “To talk about this as being the beginning of some notional ‘World War Three’ is to overlook the fact that this is about Russia invading the very specific nation of Ukraine, and the fact that Ukrainians, specifically, are going to be robbed of their country and their lives. It’s a way of eliding that specificity, and making it about all of us, and, by implication, me personally. In that sense, it’s the opposite of empathy. And it reduces a very specific political and humanitarian crisis to a sense of ambient unease or, worse, a kind of childish fantasy of living in the end times. It’s not about us; it’s about Ukrainians.”</span></p></div>
<div class="embed-content" data-embed-type="raw-html"><p><span>While the self-care discourse is deadly earnest, references to World War 3 tend to take the form of glib humour, usually some variation of “first a literal pandemic, and now a World War 3!” Sometimes, you get the impression that people are approaching the idea of nuclear annihilation with a kind of relish. There’s also been a spate of jokes about why, in light of the world ending, we shouldn’t have to go to work. While I am a deeply lazy person who thinks that slacking off is a moral good, it’s enough to make me want to scream: “shut your gob and do your job!” This whiny, babyish self-victimisation in the face of other people’s suffering is grotesque: if you want to skive off work, just do it, but don’t dress it up as a form of solidarity. Wry, world-weary apocalypticism has become the most viscerally annoying genre of internet humour. Apart from anything, it’s just boring, trite, and unfunny to be tweeting about “living through the literal end of days”, when you’re sitting cosy in your flat, ordering Deliveroo and watching Netflix. It’s an expression of real anxieties, I think, but there’s something smug about it. It’s gallows humour for people who aren’t really on the gallows.</span></p>
<p><span>“</span><span>In one sense, this register is just annoying because it’s tacky and cliched,” says Mark. “But again, it’s annoying because it just seems so lazily narcissistic. One thing I will say about the idea of the apocalypse is that it’s usually at least as much a fantasy as it is fear. As often as not, it’s a narcissistic fantasy about being a witness, and subject, of the end of everything. And when it’s invoked, as it so often is, in this world-weary, ironic register it’s a way of seeming to be maximally serious – because what could be more serious than the end of the world? – while being about as frivolous as it’s possible to be. </span><span>If people really did believe a nuclear exchange was imminent, they wouldn’t be acting all ironic and world-weary about it. Obviously, I can’t speak for the people of Ukraine, a country I have visited only briefly and know not enough about, but I am guessing world-weary irony is not the dominant affective tone on the streets of Kyiv, or in the crowded metro stations beneath them.”</span></p>
<p><span>The situation unfolding in Ukraine is stressful, anxiety-inducing and unpleasant. To feel upset by it is entirely unremarkable. But there has to be a way of showing solidarity with the Ukrainian people that doesn’t involve centring our own emotional reactions, our own terrible jokes.</span></p></div>

+ 341
- 0
cache/2022/d97914db7d2e525edc27669adbc0f917/index.html View File

@@ -0,0 +1,341 @@
<!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>A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly (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://endler.dev/2019/tinysearch/">

<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>A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly</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://endler.dev/2019/tinysearch/" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<div class="info"><p>I wrote a basic search module that you can add to a static website. It's very lightweight (50kB-100kB gzipped) and works with Hugo, Zola, and Jekyll. Only searching for entire words is supported. Try the search box on the left for a demo. <a href="https://github.com/mre/tinysearch">The code is on Github</a>.</p></div>
<p>Static site generators are magical. They combine the best of both worlds: dynamic content without sacrificing performance.</p>
<p>Over the years, this blog has been running on <a href="https://github.com/mre/mre.github.io.v1">Jekyll</a>, <a href="https://github.com/mre/mre.github.io.v2">Cobalt</a>, and, lately, <a href="https://www.getzola.org/">Zola</a>.</p>
<p>One thing I always disliked, however, was the fact that static websites don't come with "static" search engines, too. Instead, people resort to <a href="https://cse.google.com/about">custom Google searches</a>, external search engines like <a href="https://www.algolia.com/">Algolia</a>, or pure JavaScript-based solutions like <a href="https://lunrjs.com/">lunr.js</a> or <a href="http://elasticlunr.com/">elasticlunr</a>.</p>
<p>All of these work fine for most sites, but it never felt like the final answer.</p>
<p>I didn't want to add yet another dependency on Google; neither did I want to use a stand-alone web-backend like Algolia, which adds latency and is proprietary.</p>
<p>On the other side, I'm not a huge fan of JavaScript-heavy websites. For example, just the search indices that lunr creates can be <a href="https://github.com/olivernn/lunr.js/issues/268#issuecomment-304490937">multiple megabytes in size</a>. That feels lavish - even by today's bandwidth standards. On top of that, <a href="https://v8.dev/blog/cost-of-javascript-2019">parsing JavaScript is still time-consuming</a>.</p>
<p>I wanted some simple, lean, and self-contained search, that could be deployed next to my other static content.</p>
<p>As a consequence, I refrained from adding search functionality to my blog at all. That's unfortunate because, with a growing number of articles, it gets harder and harder to find relevant content.</p>
<h2 id="the-idea"><a class="anchor" href="#the-idea"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>The Idea</h2>
<p>Many years ago, in 2013, I read <a href="https://www.stavros.io/posts/bloom-filter-search-engine/">"Writing a full-text search engine using Bloom filters"</a> — and it was a revelation.</p>
<p>The idea was simple: Let's run all my blog articles through a generator that creates a tiny, self-contained search index using this magical data structure called a ✨<em>Bloom Filter</em> ✨.</p>
<h2 id="wait-what-s-a-bloom-filter"><a class="anchor" href="#wait-what-s-a-bloom-filter"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Wait, what's a Bloom Filter?</h2>
<p>A <a href="https://en.wikipedia.org/wiki/Bloom_filter">Bloom filter</a> is a space-efficient way to check if an element is in a set.</p>
<p>The trick is that it doesn't store the elements themselves; it just knows with some confidence that they were stored before. In our case, it can say with a certain <em>error rate</em> that a word is in an article.<figure><img alt="A Bloom filter stores a
'fingerprint' (a number of hash values) of all input values instead of the raw
input. The result is a low-memory-footprint data structure. This is an example
of 'hello' as an input." src="https://endler.dev/2019/tinysearch/bloomfilter.svg"><figcaption>A Bloom filter stores a 'fingerprint' (a number of hash values) of all input values instead of the raw input. The result is a low-memory-footprint data structure. This is an example of 'hello' as an input.</figcaption></figure></p>
<p>Here's the Python code from the original article that generates the Bloom filters for each post (courtesy of <a href="https://www.stavros.io">Stavros Korokithakis</a>):</p>
<pre class="language-python" data-lang="python"><code class="language-python" data-lang="python"><span>filters </span><span>= </span><span>{}
</span><span>for </span><span>name</span><span>, </span><span>words </span><span>in </span><span>split_posts</span><span>.</span><span>items</span><span>():
</span><span> filters[name] </span><span>= </span><span>BloomFilter</span><span>(</span><span>capacity</span><span>=</span><span>len</span><span>(words)</span><span>, </span><span>error_rate</span><span>=</span><span>0</span><span>.</span><span>1</span><span>)
</span><span> </span><span>for </span><span>word </span><span>in </span><span>words:
</span><span> filters[name]</span><span>.</span><span>add</span><span>(word)
</span></code></pre>
<p>The memory footprint is extremely small, thanks to <code>error_rate</code>, which allows for a negligible number of false positives.</p>
<p>I immediately knew that I wanted something like this for my homepage. My idea was to directly ship the Bloom filters and the search engine to the browser. I could finally have a small, static search without the need for a backend!</p>
<h2 id="headaches"><a class="anchor" href="#headaches"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Headaches</h2>
<p>Disillusionment came quickly.</p>
<p>I had no idea how to bundle and minimize the generated Bloom filters, let alone run them on clients. The original article briefly touches on this:</p>
<blockquote><p>You need to implement a Bloom filter algorithm on the client-side. This will probably not be much longer than the inverted index search algorithm, but it’s still probably a bit more complicated.</p></blockquote>
<p>I didn't feel confident enough in my JavaScript skills to pull this off. Back in 2013, NPM was a mere three years old, and WebPack just turned one, so I also didn't know where to look for existing solutions.</p>
<p>Unsure what to do next, my idea remained a pipe dream.</p>
<h2 id="a-new-hope"><a class="anchor" href="#a-new-hope"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>A New Hope</h2>
<p>Five years later, in 2018, the web had become a different place. Bundlers were ubiquitous, and the Node ecosystem was flourishing. One thing, in particular, revived my dreams about the tiny static search engine: <a href="https://webassembly.org/">WebAssembly</a>.</p>
<blockquote><p>WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications. [<a href="https://webassembly.org/">source</a>]</p></blockquote>
<p>This meant that I could use a language that I was familiar with to write the client-side code — Rust! 🎉</p>
<p>My journey started with a <a href="https://github.com/mre/tinysearch/commit/82c1d36835348718f04c9ca0dd2c1ebf8b19a312">prototype back in January 2018</a>. It was just a direct port of the Python version from above:</p>
<pre class="language-rust" data-lang="rust"><code class="language-rust" data-lang="rust"><span>let mut</span><span> filters </span><span>= </span><span>HashMap</span><span>::</span><span>new()</span><span>;
</span><span>for </span><span>(name</span><span>,</span><span> words) </span><span>in</span><span> articles {
</span><span> </span><span>let mut</span><span> filter </span><span>= </span><span>BloomFilter</span><span>::</span><span>with_rate(</span><span>0.1</span><span>,</span><span> words</span><span>.</span><span>len</span><span>() </span><span>as </span><span>u32</span><span>)</span><span>;
</span><span> </span><span>for</span><span> word </span><span>in</span><span> words {
</span><span> filter</span><span>.</span><span>insert</span><span>(</span><span>&amp;</span><span>word)</span><span>;
</span><span> }
</span><span> filters</span><span>.</span><span>insert</span><span>(name</span><span>,</span><span> filter)</span><span>;
</span><span>}
</span></code></pre>
<p>While I managed to create the Bloom filters for every article, I <em>still</em> had no clue how to package it for the web... until <a href="https://github.com/rustwasm/wasm-pack/commit/125431f97eecb6f3ca5122f8b345ba5b7eee94c7">wasm-pack came along in February 2018</a>.</p>
<h2 id="whoops-i-shipped-some-rust-code-to-your-browser"><a class="anchor" href="#whoops-i-shipped-some-rust-code-to-your-browser"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Whoops! I Shipped Some Rust Code To Your Browser.</h2>
<p>Now I had all the pieces of the puzzle:</p>
<ul><li>Rust — A language I was comfortable with</li><li><a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a> — A bundler for WebAssembly modules</li><li>A working prototype that served as a proof-of-concept</li></ul>
<p>The search box you see on the left side of this page is the outcome. It fully runs on Rust using WebAssembly (a.k.a the <a href="https://twitter.com/timClicks/status/1181822319620063237">RAW stack</a>). Try it now if you like.</p>
<p>There were quite a few obstacles along the way.</p>
<h2 id="bloom-filter-crates"><a class="anchor" href="#bloom-filter-crates"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Bloom Filter Crates</h2>
<p>I looked into a few Rust libraries (crates) that implement Bloom filters.</p>
<p>First, I tried jedisct1's <a href="https://github.com/jedisct1/rust-bloom-filter">rust-bloom-filter</a>, but the types didn't implement <a href="https://docs.serde.rs/serde/trait.Serialize.html">Serialize</a>/<a href="https://docs.serde.rs/serde/trait.Deserialize.html">Deserialize</a>. This meant that I could not store my generated Bloom filters inside the binary and load them on the client-side.</p>
<p>After trying a few others, I found the <a href="https://github.com/seiflotfy/rust-cuckoofilter">cuckoofilter</a> crate, which supported serialization. The behavior is similar to Bloom filters, but if you're interested in the differences, you can look at <a href="https://brilliant.org/wiki/cuckoo-filter/">this summary</a>.</p>
<p>Here's how to use it:</p>
<pre class="language-rust" data-lang="rust"><code class="language-rust" data-lang="rust"><span>let mut</span><span> cf </span><span>= </span><span>cuckoofilter</span><span>::</span><span>new()</span><span>;
</span><span>
</span><span>// Add data to the filter
</span><span>let</span><span> value</span><span>: </span><span>&amp;</span><span>str </span><span>= </span><span>"hello world"</span><span>;
</span><span>let</span><span> success </span><span>=</span><span> cf</span><span>.</span><span>add</span><span>(value)</span><span>?</span><span>;
</span><span>
</span><span>// Lookup if data was added before
</span><span>let</span><span> success </span><span>=</span><span> cf</span><span>.</span><span>contains</span><span>(value)</span><span>;
</span><span>// success ==&gt; true
</span></code></pre>
<p>Let's check the output size when bundling the filters for ten articles on my blog using cuckoo filters:</p>
<pre><code><span>~/C/p/tinysearch ❯❯❯ l storage
</span><span>Permissions Size User Date Modified Name
</span><span>.rw-r--r-- 44k mendler 24 Mar 15:42 storage
</span></code></pre>
<p><strong>44kB</strong> doesn't sound too shabby, but these are just the cuckoo filters for ten articles, serialized as a Rust binary. On top of that, we have to add the search functionality and the helper code. In total, the client-side code weighed in at <strong>216kB</strong> using vanilla wasm-pack. Too much.</p>
<h2 id="trimming-binary-size"><a class="anchor" href="#trimming-binary-size"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Trimming Binary Size</h2>
<p>After the sobering first result of 216kB for our initial prototype, we have a few options to bring the binary size down.</p>
<p>The first is following <a href="https://github.com/johnthagen">johnthagen's</a> advice on <a href="https://github.com/johnthagen/min-sized-rust">minimizing Rust binary size</a>.</p>
<p>By setting a few options in our <code>Cargo.toml</code>, we can shave off quite a few bytes:</p>
<pre><code><span>"opt-level = 'z'" =&gt; 249665 bytes
</span><span>"lto = true" =&gt; 202516 bytes
</span><span>"opt-level = 's'" =&gt; 195950 bytes
</span></code></pre>
<p>Setting <code>opt-level</code> to <code>s</code> means we trade size for speed, but we're preliminarily interested in minimal size anyway. After all, a small download size also improves performance.</p>
<p>Next, we can try <a href="https://github.com/rustwasm/wee_alloc">wee_alloc</a>, an alternative Rust allocator producing a small <code>.wasm</code> code size.</p>
<blockquote><p>It is geared towards code that makes a handful of initial dynamically sized allocations, and then performs its heavy lifting without any further allocations. This scenario requires some allocator to exist, but we are more than happy to trade allocation performance for small code size.</p></blockquote>
<p>Exactly what we want. Let's try!</p>
<pre><code><span>"wee_alloc and nightly" =&gt; 187560 bytes
</span></code></pre>
<p>We shaved off another 4% from our binary.</p>
<p>Out of curiosity, I tried to set <a href="https://doc.rust-lang.org/rustc/codegen-options/index.html#codegen-units">codegen-units</a> to 1, meaning we only use a single thread for code generation. Surprisingly, this resulted in a slightly smaller binary size.</p>
<pre><code><span>"codegen-units = 1" =&gt; 183294 bytes
</span></code></pre>
<p>Then I got word of a Wasm optimizer called <code>binaryen</code>. On macOS, it's available through homebrew:</p>
<pre><code><span>brew install binaryen
</span></code></pre>
<p>It ships a binary called <code>wasm-opt</code> and that shaved off another 15%:</p>
<pre><code><span>"wasm-opt -Oz" =&gt; 154413 bytes
</span></code></pre>
<p>Then I removed web-sys as we don't have to bind to the DOM: 152858 bytes.</p>
<p>There's a tool called <a href="https://github.com/rustwasm/twiggy">twiggy</a> to profile the code size of Wasm binaries. It printed the following output:</p>
<pre><code><span>twiggy top -n 20 pkg/tinysearch_bg.wasm
</span><span> Shallow Bytes │ Shallow % │ Item
</span><span>─────────────┼───────────┼────────────────────────────────
</span><span> 79256 ┊ 44.37% ┊ data[0]
</span><span> 13886 ┊ 7.77% ┊ "function names" subsection
</span><span> 7289 ┊ 4.08% ┊ data[1]
</span><span> 6888 ┊ 3.86% ┊ core::fmt::float::float_to_decimal_common_shortest::hdd201d50dffd0509
</span><span> 6080 ┊ 3.40% ┊ core::fmt::float::float_to_decimal_common_exact::hcb5f56a54ebe7361
</span><span> 5972 ┊ 3.34% ┊ std::sync::once::Once::call_once::{{closure}}::ha520deb2caa7e231
</span><span> 5869 ┊ 3.29% ┊ search
</span></code></pre>
<p>From what I can tell, the biggest chunk of our binary is occupied by the raw data section for our articles. Next up, we got the function headers and some float to decimal helper functions, that most likely come from deserialization.</p>
<p>Finally, I tried <a href="https://github.com/rustwasm/wasm-snip">wasm-snip</a>, which replaces a WebAssembly function's body with an <code>unreachable</code> like so, but it didn't reduce code size:</p>
<pre><code><span>wasm-snip --snip-rust-fmt-code --snip-rust-panicking-code -o pkg/tinysearch_bg_snip.wasm pkg/tinysearch_bg_opt.wasm
</span></code></pre>
<p>After tweaking with the parameters of the cuckoo filters a bit and removing <a href="https://en.wikipedia.org/wiki/Stop_words">stop words</a> from the articles, I arrived at <strong>121kB</strong> (51kB gzipped) — not bad considering the average image size on the web is <a href="https://httparchive.org/reports/state-of-images#bytesImg">around 900kB</a>. On top of that, the search functionality only gets loaded when a user clicks into the search field.</p>
<h2 id="update"><a class="anchor" href="#update"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Update</h2>
<p>Recently I moved the project from cuckoofilters to <a href="https://arxiv.org/abs/1912.08258">XOR filters</a>. I used the awesome <a href="https://github.com/ayazhafiz/xorf">xorf</a> project, which comes with built-in serde serialization. which allowed me to remove a lot of custom code.</p>
<p>With that, I could reduce the payload size by another 20-25% percent. I'm down to <strong>99kB</strong> (<strong>49kB gzipped</strong>) on my blog now. 🎉</p>
<p>The new version is released <a href="https://crates.io/crates/tinysearch">on crates.io</a> already, if you want to give it a try.</p>
<h2 id="frontend-and-glue-code"><a class="anchor" href="#frontend-and-glue-code"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Frontend- and Glue Code</h2>
<p>wasm-pack will auto-generate the JavaScript code to talk to Wasm.</p>
<p>For the search UI, I customized a few JavaScript and CSS bits from <a href="https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_autocomplete">w3schools</a>. It even has keyboard support! Now when a user enters a search query, we go through the cuckoo filter of each article and try to match the words. The results are scored by the number of hits. Thanks to my dear colleague <a href="https://github.com/jorgelbg/">Jorge Luis Betancourt</a> for adding that part.</p>
<p><img alt="Video of the search functionality" src="./anim-opt2.gif"></p>
<p>(Fun fact: this animation is about the same size as the uncompressed Wasm search itself.)</p>
<h2 id="caveats"><a class="anchor" href="#caveats"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Caveats</h2>
<p>Only whole words are matched. I would love to add prefix-search, but the binary became too big when I tried.</p>
<h2 id="usage"><a class="anchor" href="#usage"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Usage</h2>
<p>The standalone binary to create the Wasm file is called <code>tinysearch</code>. It expects a single path to a JSON file as an input:</p>
<pre><code><span>tinysearch path/to/corpus.json
</span></code></pre>
<p>This <code>corpus.json</code> contains the text you would like to index. The format is pretty straightforward:</p>
<pre class="language-json" data-lang="json"><code class="language-json" data-lang="json"><span>[
</span><span> {
</span><span> </span><span>"title"</span><span>: </span><span>"Article 1"</span><span>,
</span><span> </span><span>"url"</span><span>: </span><span>"https://example.com/article1"</span><span>,
</span><span> </span><span>"body"</span><span>: </span><span>"This is the body of article 1."
</span><span> }</span><span>,
</span><span> {
</span><span> </span><span>"title"</span><span>: </span><span>"Article 2"</span><span>,
</span><span> </span><span>"url"</span><span>: </span><span>"https://example.com/article2"</span><span>,
</span><span> </span><span>"body"</span><span>: </span><span>"This is the body of article 2."
</span><span> }
</span><span>]
</span></code></pre>
<p>You can generate this JSON file with any static site generator. <a href="https://github.com/mre/mre.github.io/tree/1c731717b48afb584e54ca4dd5fd649f9b74e51c/templates">Here's my version for Zola</a>:</p>
<pre class="language-t" data-lang="t"><code class="language-t" data-lang="t"><span>{</span><span>% </span><span>set </span><span>section </span><span>= </span><span>get_section</span><span>(</span><span>path</span><span>=</span><span>"_index.md"</span><span>) </span><span>%</span><span>}
</span><span>
</span><span>[
</span><span> {</span><span>%- </span><span>for </span><span>post in </span><span>section</span><span>.</span><span>pages </span><span>-%</span><span>}
</span><span> {</span><span>% </span><span>if </span><span>not </span><span>post</span><span>.</span><span>draft </span><span>%</span><span>}
</span><span> {
</span><span> </span><span>"title"</span><span>: </span><span>{{ </span><span>post</span><span>.</span><span>title </span><span>| </span><span>striptags </span><span>| </span><span>json_encode </span><span>| </span><span>safe </span><span>}}</span><span>,
</span><span> </span><span>"url"</span><span>: </span><span>{{ </span><span>post</span><span>.</span><span>permalink </span><span>| </span><span>json_encode </span><span>| </span><span>safe </span><span>}}</span><span>,
</span><span> </span><span>"body"</span><span>: </span><span>{{ </span><span>post</span><span>.</span><span>content </span><span>| </span><span>striptags </span><span>| </span><span>json_encode </span><span>| </span><span>safe </span><span>}}
</span><span> }
</span><span> {</span><span>% </span><span>if </span><span>not </span><span>loop</span><span>.</span><span>last </span><span>%</span><span>}</span><span>,</span><span>{</span><span>% </span><span>endif </span><span>%</span><span>}
</span><span> {</span><span>% </span><span>endif </span><span>%</span><span>}
</span><span> {</span><span>%- </span><span>endfor </span><span>-%</span><span>}
</span><span>]
</span></code></pre>
<p>I'm pretty sure that the Jekyll version looks quite similar. <a href="https://learn.cloudcannon.com/jekyll/output-json/">Here's a starting point</a>. If you get something working for your static site generator, <a href="https://github.com/tinysearch/tinysearch/tree/master/howto">please let me know</a>.</p>
<h2 id="observations"><a class="anchor" href="#observations"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Observations</h2>
<ul><li>This is still the wild west: unstable features, nightly Rust, documentation gets outdated almost every day.<br> Bring your thinking cap!</li><li>Creating a product out of a good idea is a lot of work. One has to pay attention to many factors: ease-of-use, generality, maintainability, documentation, and so on.</li><li>Rust is very good at removing dead code, so you usually don't pay for what you don't use. I would still advise you to be very conservative about the dependencies you add to a Wasm binary because it's tempting to add features that you don't need and which will add to the binary size. For example, I used <a href="https://github.com/TeXitoi/structopt">StructOpt</a> during testing, and I had a <code>main()</code> function that was parsing these command-line arguments. This was not necessary for Wasm, so I removed it later.</li><li>I understand that not everyone wants to write Rust code. It's <a href="https://endler.dev/2017/go-vs-rust/">complicated to get started with</a>, but the cool thing is that you can use almost any other language, too. For example, you can write Go code and transpile to Wasm, or maybe you prefer PHP or Haskell. There is support for <a href="https://github.com/appcypher/awesome-wasm-langs">many languages</a> already.</li><li>A lot of people dismiss WebAssembly as a toy technology. They couldn't be further from the truth. In my opinion, WebAssembly will revolutionize the way we build products for the web and beyond. What was very hard just two years ago is now easy: shipping code in any language to every browser. I'm super excited about its future.</li><li>If you're looking for a standalone, self-hosted search index for your company website, check out <a href="https://journal.valeriansaliou.name/announcing-sonic-a-super-light-alternative-to-elasticsearch/">sonic</a>. Also check out <a href="https://github.com/jameslittle230/stork">stork</a> as an alternative.</li></ul>
<div class="info"><p>✨<strong>WOW!</strong> This tool getting quite a bit of traction lately.✨‍</p><p>I don't run ads on this website, but if you like these kind of experiments, please consider <a href="https://github.com/sponsors/mre/">sponsoring me on Github</a>. This allows me to write more tools like this in the future.</p><p>Also, if you're interested in <strong>hands-on Rust consulting</strong>, <a href="https://github.com/sponsors/mre/sponsorships?sponsor=mre&amp;tier_id=78832">pick a date from my calendar</a> and we can talk about how I can help .</p></div>
<h2 id="try-it"><a class="anchor" href="#try-it"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Try it!</h2>
<p>The code for <a href="https://github.com/mre/tinysearch">tinysearch is on Github</a>.</p>
<p>Please be aware of these limitations:</p>
<ul><li><strong>Only searches for entire words.</strong> There are no search suggestions. The reason is that prefix search blows up binary size like <a href="https://www.youtube.com/watch?v=b6u9WJ01Oxs">Mentos and Diet Coke</a>.</li><li>Since we bundle all search indices for all articles into one static binary, I <strong>only recommend to use it for low- to medium-sized websites</strong>. Expect around 4kB (non-compressed) per article.</li><li><strike>The <strong>compile times are abysmal</strong> at the moment (around 1.5 minutes after a fresh install on my machine), mainly because we're compiling the Rust crate from scratch every time we rebuild the index.</strike><br> Update: This is mostly fixed thanks to the awesome work of <a href="https://github.com/CephalonRho">CephalonRho</a> in PR <a href="https://github.com/mre/tinysearch/pull/13">#13</a>. Thanks again!</li></ul>
<p>The final Wasm code is laser-fast because we save the roundtrips to a search-server. The instant feedback loop feels more like filtering a list than searching through posts. It can even work fully offline, which might be nice if you like to bundle it with an app.</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>

+ 78
- 0
cache/2022/d97914db7d2e525edc27669adbc0f917/index.md View File

@@ -0,0 +1,78 @@
title: A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly
url: https://endler.dev/2019/tinysearch/
hash_url: d97914db7d2e525edc27669adbc0f917

<div class="info"><p>I wrote a basic search module that you can add to a static website. It's very lightweight (50kB-100kB gzipped) and works with Hugo, Zola, and Jekyll. Only searching for entire words is supported. Try the search box on the left for a demo. <a href="https://github.com/mre/tinysearch">The code is on Github</a>.</p></div><p>Static site generators are magical. They combine the best of both worlds: dynamic content without sacrificing performance.</p><p>Over the years, this blog has been running on <a href="https://github.com/mre/mre.github.io.v1">Jekyll</a>, <a href="https://github.com/mre/mre.github.io.v2">Cobalt</a>, and, lately, <a href="https://www.getzola.org/">Zola</a>.</p><p>One thing I always disliked, however, was the fact that static websites don't come with "static" search engines, too. Instead, people resort to <a href="https://cse.google.com/about">custom Google searches</a>, external search engines like <a href="https://www.algolia.com/">Algolia</a>, or pure JavaScript-based solutions like <a href="https://lunrjs.com/">lunr.js</a> or <a href="http://elasticlunr.com/">elasticlunr</a>.</p><p>All of these work fine for most sites, but it never felt like the final answer.</p><p>I didn't want to add yet another dependency on Google; neither did I want to use a stand-alone web-backend like Algolia, which adds latency and is proprietary.</p><p>On the other side, I'm not a huge fan of JavaScript-heavy websites. For example, just the search indices that lunr creates can be <a href="https://github.com/olivernn/lunr.js/issues/268#issuecomment-304490937">multiple megabytes in size</a>. That feels lavish - even by today's bandwidth standards. On top of that, <a href="https://v8.dev/blog/cost-of-javascript-2019">parsing JavaScript is still time-consuming</a>.</p><p>I wanted some simple, lean, and self-contained search, that could be deployed next to my other static content.</p><p>As a consequence, I refrained from adding search functionality to my blog at all. That's unfortunate because, with a growing number of articles, it gets harder and harder to find relevant content.</p><h2 id="the-idea"><a class="anchor" href="#the-idea"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>The Idea</h2><p>Many years ago, in 2013, I read <a href="https://www.stavros.io/posts/bloom-filter-search-engine/">"Writing a full-text search engine using Bloom filters"</a> — and it was a revelation.</p><p>The idea was simple: Let's run all my blog articles through a generator that creates a tiny, self-contained search index using this magical data structure called a ✨<em>Bloom Filter</em> ✨.</p><h2 id="wait-what-s-a-bloom-filter"><a class="anchor" href="#wait-what-s-a-bloom-filter"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Wait, what's a Bloom Filter?</h2><p>A <a href="https://en.wikipedia.org/wiki/Bloom_filter">Bloom filter</a> is a space-efficient way to check if an element is in a set.</p><p>The trick is that it doesn't store the elements themselves; it just knows with some confidence that they were stored before. In our case, it can say with a certain <em>error rate</em> that a word is in an article.<figure><img alt="A Bloom filter stores a
'fingerprint' (a number of hash values) of all input values instead of the raw
input. The result is a low-memory-footprint data structure. This is an example
of 'hello' as an input." src="https://endler.dev/2019/tinysearch/bloomfilter.svg"><figcaption>A Bloom filter stores a 'fingerprint' (a number of hash values) of all input values instead of the raw input. The result is a low-memory-footprint data structure. This is an example of 'hello' as an input.</figcaption></figure></p><p>Here's the Python code from the original article that generates the Bloom filters for each post (courtesy of <a href="https://www.stavros.io">Stavros Korokithakis</a>):</p><pre class="language-python" data-lang="python"><code class="language-python" data-lang="python"><span>filters </span><span>= </span><span>{}
</span><span>for </span><span>name</span><span>, </span><span>words </span><span>in </span><span>split_posts</span><span>.</span><span>items</span><span>():
</span><span> filters[name] </span><span>= </span><span>BloomFilter</span><span>(</span><span>capacity</span><span>=</span><span>len</span><span>(words)</span><span>, </span><span>error_rate</span><span>=</span><span>0</span><span>.</span><span>1</span><span>)
</span><span> </span><span>for </span><span>word </span><span>in </span><span>words:
</span><span> filters[name]</span><span>.</span><span>add</span><span>(word)
</span></code></pre><p>The memory footprint is extremely small, thanks to <code>error_rate</code>, which allows for a negligible number of false positives.</p><p>I immediately knew that I wanted something like this for my homepage. My idea was to directly ship the Bloom filters and the search engine to the browser. I could finally have a small, static search without the need for a backend!</p><h2 id="headaches"><a class="anchor" href="#headaches"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Headaches</h2><p>Disillusionment came quickly.</p><p>I had no idea how to bundle and minimize the generated Bloom filters, let alone run them on clients. The original article briefly touches on this:</p><blockquote><p>You need to implement a Bloom filter algorithm on the client-side. This will probably not be much longer than the inverted index search algorithm, but it’s still probably a bit more complicated.</p></blockquote><p>I didn't feel confident enough in my JavaScript skills to pull this off. Back in 2013, NPM was a mere three years old, and WebPack just turned one, so I also didn't know where to look for existing solutions.</p><p>Unsure what to do next, my idea remained a pipe dream.</p><h2 id="a-new-hope"><a class="anchor" href="#a-new-hope"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>A New Hope</h2><p>Five years later, in 2018, the web had become a different place. Bundlers were ubiquitous, and the Node ecosystem was flourishing. One thing, in particular, revived my dreams about the tiny static search engine: <a href="https://webassembly.org/">WebAssembly</a>.</p><blockquote><p>WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications. [<a href="https://webassembly.org/">source</a>]</p></blockquote><p>This meant that I could use a language that I was familiar with to write the client-side code — Rust! 🎉</p><p>My journey started with a <a href="https://github.com/mre/tinysearch/commit/82c1d36835348718f04c9ca0dd2c1ebf8b19a312">prototype back in January 2018</a>. It was just a direct port of the Python version from above:</p><pre class="language-rust" data-lang="rust"><code class="language-rust" data-lang="rust"><span>let mut</span><span> filters </span><span>= </span><span>HashMap</span><span>::</span><span>new()</span><span>;
</span><span>for </span><span>(name</span><span>,</span><span> words) </span><span>in</span><span> articles {
</span><span> </span><span>let mut</span><span> filter </span><span>= </span><span>BloomFilter</span><span>::</span><span>with_rate(</span><span>0.1</span><span>,</span><span> words</span><span>.</span><span>len</span><span>() </span><span>as </span><span>u32</span><span>)</span><span>;
</span><span> </span><span>for</span><span> word </span><span>in</span><span> words {
</span><span> filter</span><span>.</span><span>insert</span><span>(</span><span>&amp;</span><span>word)</span><span>;
</span><span> }
</span><span> filters</span><span>.</span><span>insert</span><span>(name</span><span>,</span><span> filter)</span><span>;
</span><span>}
</span></code></pre><p>While I managed to create the Bloom filters for every article, I <em>still</em> had no clue how to package it for the web... until <a href="https://github.com/rustwasm/wasm-pack/commit/125431f97eecb6f3ca5122f8b345ba5b7eee94c7">wasm-pack came along in February 2018</a>.</p><h2 id="whoops-i-shipped-some-rust-code-to-your-browser"><a class="anchor" href="#whoops-i-shipped-some-rust-code-to-your-browser"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Whoops! I Shipped Some Rust Code To Your Browser.</h2><p>Now I had all the pieces of the puzzle:</p><ul><li>Rust — A language I was comfortable with</li><li><a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a> — A bundler for WebAssembly modules</li><li>A working prototype that served as a proof-of-concept</li></ul><p>The search box you see on the left side of this page is the outcome. It fully runs on Rust using WebAssembly (a.k.a the <a href="https://twitter.com/timClicks/status/1181822319620063237">RAW stack</a>). Try it now if you like.</p><p>There were quite a few obstacles along the way.</p><h2 id="bloom-filter-crates"><a class="anchor" href="#bloom-filter-crates"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Bloom Filter Crates</h2><p>I looked into a few Rust libraries (crates) that implement Bloom filters.</p><p>First, I tried jedisct1's <a href="https://github.com/jedisct1/rust-bloom-filter">rust-bloom-filter</a>, but the types didn't implement <a href="https://docs.serde.rs/serde/trait.Serialize.html">Serialize</a>/<a href="https://docs.serde.rs/serde/trait.Deserialize.html">Deserialize</a>. This meant that I could not store my generated Bloom filters inside the binary and load them on the client-side.</p><p>After trying a few others, I found the <a href="https://github.com/seiflotfy/rust-cuckoofilter">cuckoofilter</a> crate, which supported serialization. The behavior is similar to Bloom filters, but if you're interested in the differences, you can look at <a href="https://brilliant.org/wiki/cuckoo-filter/">this summary</a>.</p><p>Here's how to use it:</p><pre class="language-rust" data-lang="rust"><code class="language-rust" data-lang="rust"><span>let mut</span><span> cf </span><span>= </span><span>cuckoofilter</span><span>::</span><span>new()</span><span>;
</span><span>
</span><span>// Add data to the filter
</span><span>let</span><span> value</span><span>: </span><span>&amp;</span><span>str </span><span>= </span><span>"hello world"</span><span>;
</span><span>let</span><span> success </span><span>=</span><span> cf</span><span>.</span><span>add</span><span>(value)</span><span>?</span><span>;
</span><span>
</span><span>// Lookup if data was added before
</span><span>let</span><span> success </span><span>=</span><span> cf</span><span>.</span><span>contains</span><span>(value)</span><span>;
</span><span>// success ==&gt; true
</span></code></pre><p>Let's check the output size when bundling the filters for ten articles on my blog using cuckoo filters:</p><pre><code><span>~/C/p/tinysearch ❯❯❯ l storage
</span><span>Permissions Size User Date Modified Name
</span><span>.rw-r--r-- 44k mendler 24 Mar 15:42 storage
</span></code></pre><p><strong>44kB</strong> doesn't sound too shabby, but these are just the cuckoo filters for ten articles, serialized as a Rust binary. On top of that, we have to add the search functionality and the helper code. In total, the client-side code weighed in at <strong>216kB</strong> using vanilla wasm-pack. Too much.</p><h2 id="trimming-binary-size"><a class="anchor" href="#trimming-binary-size"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Trimming Binary Size</h2><p>After the sobering first result of 216kB for our initial prototype, we have a few options to bring the binary size down.</p><p>The first is following <a href="https://github.com/johnthagen">johnthagen's</a> advice on <a href="https://github.com/johnthagen/min-sized-rust">minimizing Rust binary size</a>.</p><p>By setting a few options in our <code>Cargo.toml</code>, we can shave off quite a few bytes:</p><pre><code><span>"opt-level = 'z'" =&gt; 249665 bytes
</span><span>"lto = true" =&gt; 202516 bytes
</span><span>"opt-level = 's'" =&gt; 195950 bytes
</span></code></pre><p>Setting <code>opt-level</code> to <code>s</code> means we trade size for speed, but we're preliminarily interested in minimal size anyway. After all, a small download size also improves performance.</p><p>Next, we can try <a href="https://github.com/rustwasm/wee_alloc">wee_alloc</a>, an alternative Rust allocator producing a small <code>.wasm</code> code size.</p><blockquote><p>It is geared towards code that makes a handful of initial dynamically sized allocations, and then performs its heavy lifting without any further allocations. This scenario requires some allocator to exist, but we are more than happy to trade allocation performance for small code size.</p></blockquote><p>Exactly what we want. Let's try!</p><pre><code><span>"wee_alloc and nightly" =&gt; 187560 bytes
</span></code></pre><p>We shaved off another 4% from our binary.</p><p>Out of curiosity, I tried to set <a href="https://doc.rust-lang.org/rustc/codegen-options/index.html#codegen-units">codegen-units</a> to 1, meaning we only use a single thread for code generation. Surprisingly, this resulted in a slightly smaller binary size.</p><pre><code><span>"codegen-units = 1" =&gt; 183294 bytes
</span></code></pre><p>Then I got word of a Wasm optimizer called <code>binaryen</code>. On macOS, it's available through homebrew:</p><pre><code><span>brew install binaryen
</span></code></pre><p>It ships a binary called <code>wasm-opt</code> and that shaved off another 15%:</p><pre><code><span>"wasm-opt -Oz" =&gt; 154413 bytes
</span></code></pre><p>Then I removed web-sys as we don't have to bind to the DOM: 152858 bytes.</p><p>There's a tool called <a href="https://github.com/rustwasm/twiggy">twiggy</a> to profile the code size of Wasm binaries. It printed the following output:</p><pre><code><span>twiggy top -n 20 pkg/tinysearch_bg.wasm
</span><span> Shallow Bytes │ Shallow % │ Item
</span><span>─────────────┼───────────┼────────────────────────────────
</span><span> 79256 ┊ 44.37% ┊ data[0]
</span><span> 13886 ┊ 7.77% ┊ "function names" subsection
</span><span> 7289 ┊ 4.08% ┊ data[1]
</span><span> 6888 ┊ 3.86% ┊ core::fmt::float::float_to_decimal_common_shortest::hdd201d50dffd0509
</span><span> 6080 ┊ 3.40% ┊ core::fmt::float::float_to_decimal_common_exact::hcb5f56a54ebe7361
</span><span> 5972 ┊ 3.34% ┊ std::sync::once::Once::call_once::{{closure}}::ha520deb2caa7e231
</span><span> 5869 ┊ 3.29% ┊ search
</span></code></pre><p>From what I can tell, the biggest chunk of our binary is occupied by the raw data section for our articles. Next up, we got the function headers and some float to decimal helper functions, that most likely come from deserialization.</p><p>Finally, I tried <a href="https://github.com/rustwasm/wasm-snip">wasm-snip</a>, which replaces a WebAssembly function's body with an <code>unreachable</code> like so, but it didn't reduce code size:</p><pre><code><span>wasm-snip --snip-rust-fmt-code --snip-rust-panicking-code -o pkg/tinysearch_bg_snip.wasm pkg/tinysearch_bg_opt.wasm
</span></code></pre><p>After tweaking with the parameters of the cuckoo filters a bit and removing <a href="https://en.wikipedia.org/wiki/Stop_words">stop words</a> from the articles, I arrived at <strong>121kB</strong> (51kB gzipped) — not bad considering the average image size on the web is <a href="https://httparchive.org/reports/state-of-images#bytesImg">around 900kB</a>. On top of that, the search functionality only gets loaded when a user clicks into the search field.</p><h2 id="update"><a class="anchor" href="#update"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Update</h2><p>Recently I moved the project from cuckoofilters to <a href="https://arxiv.org/abs/1912.08258">XOR filters</a>. I used the awesome <a href="https://github.com/ayazhafiz/xorf">xorf</a> project, which comes with built-in serde serialization. which allowed me to remove a lot of custom code.</p><p>With that, I could reduce the payload size by another 20-25% percent. I'm down to <strong>99kB</strong> (<strong>49kB gzipped</strong>) on my blog now. 🎉</p><p>The new version is released <a href="https://crates.io/crates/tinysearch">on crates.io</a> already, if you want to give it a try.</p><h2 id="frontend-and-glue-code"><a class="anchor" href="#frontend-and-glue-code"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Frontend- and Glue Code</h2><p>wasm-pack will auto-generate the JavaScript code to talk to Wasm.</p><p>For the search UI, I customized a few JavaScript and CSS bits from <a href="https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_autocomplete">w3schools</a>. It even has keyboard support! Now when a user enters a search query, we go through the cuckoo filter of each article and try to match the words. The results are scored by the number of hits. Thanks to my dear colleague <a href="https://github.com/jorgelbg/">Jorge Luis Betancourt</a> for adding that part.</p><p><img alt="Video of the search functionality" src="./anim-opt2.gif"></p><p>(Fun fact: this animation is about the same size as the uncompressed Wasm search itself.)</p><h2 id="caveats"><a class="anchor" href="#caveats"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Caveats</h2><p>Only whole words are matched. I would love to add prefix-search, but the binary became too big when I tried.</p><h2 id="usage"><a class="anchor" href="#usage"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Usage</h2><p>The standalone binary to create the Wasm file is called <code>tinysearch</code>. It expects a single path to a JSON file as an input:</p><pre><code><span>tinysearch path/to/corpus.json
</span></code></pre><p>This <code>corpus.json</code> contains the text you would like to index. The format is pretty straightforward:</p><pre class="language-json" data-lang="json"><code class="language-json" data-lang="json"><span>[
</span><span> {
</span><span> </span><span>"title"</span><span>: </span><span>"Article 1"</span><span>,
</span><span> </span><span>"url"</span><span>: </span><span>"https://example.com/article1"</span><span>,
</span><span> </span><span>"body"</span><span>: </span><span>"This is the body of article 1."
</span><span> }</span><span>,
</span><span> {
</span><span> </span><span>"title"</span><span>: </span><span>"Article 2"</span><span>,
</span><span> </span><span>"url"</span><span>: </span><span>"https://example.com/article2"</span><span>,
</span><span> </span><span>"body"</span><span>: </span><span>"This is the body of article 2."
</span><span> }
</span><span>]
</span></code></pre><p>You can generate this JSON file with any static site generator. <a href="https://github.com/mre/mre.github.io/tree/1c731717b48afb584e54ca4dd5fd649f9b74e51c/templates">Here's my version for Zola</a>:</p><pre class="language-t" data-lang="t"><code class="language-t" data-lang="t"><span>{</span><span>% </span><span>set </span><span>section </span><span>= </span><span>get_section</span><span>(</span><span>path</span><span>=</span><span>"_index.md"</span><span>) </span><span>%</span><span>}
</span><span>
</span><span>[
</span><span> {</span><span>%- </span><span>for </span><span>post in </span><span>section</span><span>.</span><span>pages </span><span>-%</span><span>}
</span><span> {</span><span>% </span><span>if </span><span>not </span><span>post</span><span>.</span><span>draft </span><span>%</span><span>}
</span><span> {
</span><span> </span><span>"title"</span><span>: </span><span>{{ </span><span>post</span><span>.</span><span>title </span><span>| </span><span>striptags </span><span>| </span><span>json_encode </span><span>| </span><span>safe </span><span>}}</span><span>,
</span><span> </span><span>"url"</span><span>: </span><span>{{ </span><span>post</span><span>.</span><span>permalink </span><span>| </span><span>json_encode </span><span>| </span><span>safe </span><span>}}</span><span>,
</span><span> </span><span>"body"</span><span>: </span><span>{{ </span><span>post</span><span>.</span><span>content </span><span>| </span><span>striptags </span><span>| </span><span>json_encode </span><span>| </span><span>safe </span><span>}}
</span><span> }
</span><span> {</span><span>% </span><span>if </span><span>not </span><span>loop</span><span>.</span><span>last </span><span>%</span><span>}</span><span>,</span><span>{</span><span>% </span><span>endif </span><span>%</span><span>}
</span><span> {</span><span>% </span><span>endif </span><span>%</span><span>}
</span><span> {</span><span>%- </span><span>endfor </span><span>-%</span><span>}
</span><span>]
</span></code></pre><p>I'm pretty sure that the Jekyll version looks quite similar. <a href="https://learn.cloudcannon.com/jekyll/output-json/">Here's a starting point</a>. If you get something working for your static site generator, <a href="https://github.com/tinysearch/tinysearch/tree/master/howto">please let me know</a>.</p><h2 id="observations"><a class="anchor" href="#observations"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Observations</h2><ul><li>This is still the wild west: unstable features, nightly Rust, documentation gets outdated almost every day.<br> Bring your thinking cap!</li><li>Creating a product out of a good idea is a lot of work. One has to pay attention to many factors: ease-of-use, generality, maintainability, documentation, and so on.</li><li>Rust is very good at removing dead code, so you usually don't pay for what you don't use. I would still advise you to be very conservative about the dependencies you add to a Wasm binary because it's tempting to add features that you don't need and which will add to the binary size. For example, I used <a href="https://github.com/TeXitoi/structopt">StructOpt</a> during testing, and I had a <code>main()</code> function that was parsing these command-line arguments. This was not necessary for Wasm, so I removed it later.</li><li>I understand that not everyone wants to write Rust code. It's <a href="https://endler.dev/2017/go-vs-rust/">complicated to get started with</a>, but the cool thing is that you can use almost any other language, too. For example, you can write Go code and transpile to Wasm, or maybe you prefer PHP or Haskell. There is support for <a href="https://github.com/appcypher/awesome-wasm-langs">many languages</a> already.</li><li>A lot of people dismiss WebAssembly as a toy technology. They couldn't be further from the truth. In my opinion, WebAssembly will revolutionize the way we build products for the web and beyond. What was very hard just two years ago is now easy: shipping code in any language to every browser. I'm super excited about its future.</li><li>If you're looking for a standalone, self-hosted search index for your company website, check out <a href="https://journal.valeriansaliou.name/announcing-sonic-a-super-light-alternative-to-elasticsearch/">sonic</a>. Also check out <a href="https://github.com/jameslittle230/stork">stork</a> as an alternative.</li></ul><div class="info"><p>✨<strong>WOW!</strong> This tool getting quite a bit of traction lately.✨‍</p><p>I don't run ads on this website, but if you like these kind of experiments, please consider <a href="https://github.com/sponsors/mre/">sponsoring me on Github</a>. This allows me to write more tools like this in the future.</p><p>Also, if you're interested in <strong>hands-on Rust consulting</strong>, <a href="https://github.com/sponsors/mre/sponsorships?sponsor=mre&amp;tier_id=78832">pick a date from my calendar</a> and we can talk about how I can help .</p></div><h2 id="try-it"><a class="anchor" href="#try-it"> <svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path> </svg> </a>Try it!</h2><p>The code for <a href="https://github.com/mre/tinysearch">tinysearch is on Github</a>.</p><p>Please be aware of these limitations:</p><ul><li><strong>Only searches for entire words.</strong> There are no search suggestions. The reason is that prefix search blows up binary size like <a href="https://www.youtube.com/watch?v=b6u9WJ01Oxs">Mentos and Diet Coke</a>.</li><li>Since we bundle all search indices for all articles into one static binary, I <strong>only recommend to use it for low- to medium-sized websites</strong>. Expect around 4kB (non-compressed) per article.</li><li><strike>The <strong>compile times are abysmal</strong> at the moment (around 1.5 minutes after a fresh install on my machine), mainly because we're compiling the Rust crate from scratch every time we rebuild the index.</strike><br> Update: This is mostly fixed thanks to the awesome work of <a href="https://github.com/CephalonRho">CephalonRho</a> in PR <a href="https://github.com/mre/tinysearch/pull/13">#13</a>. Thanks again!</li></ul><p>The final Wasm code is laser-fast because we save the roundtrips to a search-server. The instant feedback loop feels more like filtering a list than searching through posts. It can even work fully offline, which might be nice if you like to bundle it with an app.</p>

+ 203
- 0
cache/2022/d9af1ba02055491fc25b6849b8fd65d0/index.html View File

@@ -0,0 +1,203 @@
<!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>Ma pratique du prix libre et conscient (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://david.mercereau.info/ma-pratique-du-prix-libre-et-conscient/">

<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>Ma pratique du prix libre et conscient</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://david.mercereau.info/ma-pratique-du-prix-libre-et-conscient/" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p>Cela fait maintenant plusieurs années que je pratique le prix libre, et plus particulièrement le prix « libre et conscient », dans divers activités. Étudiant déjà, je l’ai pratiqué dans une activité d’auto-entrepreneur dans l’<a rel="noreferrer noopener" href="https://web.archive.org/web/20130610061845/http://h3w.fr/" data-type="URL" data-id="https://web.archive.org/web/20130610061845/http://h3w.fr/" target="_blank">hébergement de site internet</a>. A l’heure actuelle 95 % de mon activité économique est à prix libre et conscient (<a href="https://retzo.net/">service informatique</a>, <a rel="noreferrer noopener" href="https://david.mercereau.info/?s=Comprendre%20et%20concevoir" data-type="URL" data-id="https://david.mercereau.info/?s=Comprendre%20et%20concevoir" target="_blank">formation</a>/<a rel="noreferrer noopener" href="https://david.mercereau.info/accompagnement-autonomie-electrique/" data-type="URL" data-id="https://david.mercereau.info/accompagnement-autonomie-electrique/" target="_blank">accompagnement</a> à l’énergie solaire…). Ma démarche a été fortement enrichie / inspiré par un stage de clown que j’ai fait avec Maëlle et Thibaut de la coopérative la Dynamo.</p>
<h1><span id="Cest_quoinbsp">C’est quoi ?</span></h1>
<p>Il s’agit d’une invitation à estimer le prix au plus juste en fonction de vos moyens, de la valeur de ce que vous estimez avoir reçue, de ce que vous aurez perçu de mes besoins… Après discussion sur les coûts du bien/service, il vous sera demandé de fixer librement une participation, sans aucune justification.</p>
<p>Beaucoup assimilent le prix libre à la quasi gratuité et, de fait, ne proposent pas de bien/service à prix libre car ils pensent ne pas pouvoir se rémunérer. Or je pense que le prix libre <em>et conscient</em> permet de vivre décemment d’une activité.</p>
<h1><span id="Ma_pratique">Ma pratique</span></h1>
<p><strong>La pédagogie et la transparence</strong>, c’est la clé pour que ça fonctionne. De mon côté voici comment je fonctionne dans l’exemple d’une formation. Cela commence par un document que je rédige pour expliquer la démarche, dans celui-ci j’explique :</p>
<ul><li>Combien ça m’a coûté (en temps, en argent) ;</li><li>Je donne des références de prix à service équivalent ailleurs (à prix fixe) ;</li><li>J’exprime mon besoin / mes attentes sur une fourchette globale (sur le groupe, pas sur l’individu) ;</li><li>J’explique comment ça va se passer le jour J pour que chacun puisse me donner sa participation/contribution (mot à préférer à « argent »… peu de gens se sentent à l’aise avec l’argent ;</li><li><a href="https://david.mercereau.info/wp-content/uploads/2020/02/Participation-libre-et-consciente-Autonomie-%C3%A9lectrique-OB-2.pdf" data-type="URL" data-id="https://david.mercereau.info/wp-content/uploads/2020/02/Participation-libre-et-consciente-Autonomie-%C3%A9lectrique-OB-2.pdf">Exemple de document ici</a> ;</li></ul>
<p>Une fois ce document établi, préalablement au jour J :</p>
<ul><li>Sur « l’annonce » de l’évènement, il est bien sûr précisé que celui-ci est à « prix libre et conscient » et le document est fourni.</li><li>Les inscrits reçoivent par e-mail un rappel du document qui explique la démarche quelques semaines avant.</li></ul>
<p>Le jour J</p>
<ul><li>A l’<strong>accueil</strong>, je <strong>rappelle</strong> ce qui est dit dans le document, mes attentes, comment ça va se passer… je répète que si t’en a un peu plus dans les poches au moment T, c’est le moment de compenser pour ceux qui ne peuvent pas donner beaucoup mais qui veulent/ont besoin de l’accès à ce service (sorte de solidarité auto-gérée) ;</li><li>Je glisse pas du tout discrètement <strong>quelques</strong> feuilles du <strong>document</strong> imprimé : si certains n’ont pas lu le document les 2 premières fois… ça arrive… souvent…</li><li>A la fin de la formation je <strong>prends 5 minutes</strong> avec chaque participant, ce qui permet a chacun de me remettre sa <strong>contribution</strong> / son prix libre mais aussi d’avoir un retour individuel ;</li><li>Ensuite, j’<strong>annonce le montant global au groupe</strong>, mon ressenti par rapport à ça et chacun est libre d’ajuster sa contribution à la hausse ou à la baisse s’il estime que c’est trop ou trop peu…</li></ul>
<p>Note : Sur des évènements avec des jauges limitées, et étant donné que le prix libre ne peut être demandé préalablement, il est pour moi nécessaire de demander un « acompte » pour réserver sa place (même minime, 10€ ça suffit, et vous pouvez préciser qu’il est remboursable au paiement du prix libre à la fin de la formation) tout ça dans le but d’engager la personne. Sans quoi il n’est pas rare de constater de multiple désistements de dernière minutes (c’est étonnant le nombre de parents proches qui ont des soucis de santé la veille de la formation…).</p>
<h1><span id="Retour_dexperience"><strong>Retour d’expérience</strong></span></h1>
<h2><span id="Le_face_a_face"><strong>Le face à face</strong></span></h2>
<p>Mettre une boîte à l’entrée ou à la sortie, faire payer sa part par internet devant un écran et non un humain ça ne fonctionne pas / pas bien de mon expérience. Lors de ma première expérience d’étudiant ou je demandais une participation libre pour un service d’hébergement de site internet, par formulaire internet, il était très fréquent d’avoir des clients à 0,01€. Je suis persuadé que ces mêmes personnes, si elles n’avaient pas eu affaire à un clavier mais à une personne, ne se seraient pas permis de verser si peu pour un service/travail. Prendre 1 minute pour recevoir la contribution, en face à face et en main propre, ça fonctionne bien mieux. En effet, la personne est face à ses responsabilités / ses choix (sans pour autant avoir à se justifier).</p>
<p>Sur un festival/un spectacle à prix libre, ce qui fonctionne mieux, c’est d’avoir quelqu’un en charge des entrées (même si c’est libre et à prix libre) qui fait de la pédagogie et qui se confronte aux gens. De cette façon, en face à face, confronté à un humain et non à une boîte/un formulaire internet, la participation est plus juste.</p>
<p>C’est aussi donner la responsabilité au groupe de la réussite d’un service/d’un évènement que de les impliquer collectivement dans son coût.</p>
<h2><span id="La_fourchette_de_prix">La fourchette de prix</span></h2>
<p>Afficher un prix indicatif, une fourchette fige déjà les choses, il sera difficile pour la personne de sortir/s’éloigner du prix indicatif/la fourchette. Mais c’est une solution moins coûteuse en temps (pédagogie), qui convient bien pour de petits montants / quand on est confronté à des clients multiples et non réguliers. Le bon compromis serait de dire à chaque fois qu’on donne une fourchette, que celle-ci est indicative, qu’on peut en sortir …</p>
<h2><span id="Facilite_le_troc_dautres_echanges">Facilite le troc / d’autres échanges</span></h2>
<p>II n’est pas rare, vu qu’on a ouvert la discussion de la reconnaissance, qu’une personne me fasse une proposition autre que de l’argent (des légumes pour les maraîchers, des massages pour les masseurs…) j’essaie de répondre positivement autant que faire se peut, et bien entendu si l’échange est équilibré / que j’ai besoin dans l’année du bien/service qu’il me propose.</p>
<h2><span id="La_reconnaissance">La reconnaissance</span></h2>
<p>Un prix versé pour un service rendu est une forme de reconnaissance, reconnaissance que l’on peut assimiler à un « Merci ». Est-ce que ce « merci » n’est pas d’autant plus grand/beau s’il émerge d’une réelle volonté de remercier ? Personnellement, je suis d’autant plus touché quand cette reconnaissance par le prix est volontaire.</p>
<p>98% du temps, les gens donnent un prix juste, et pour les 2% qui ne le font pas, c’est pas grave car c’est sur le global (du groupe) que le prix doit être « juste ». De mon côté je constate qu’il est très souvent au dessus de ce que j’aurais osé annoncer comme prix.</p>
<h2><span id="Vecteur_dinteret_originalite">Vecteur d’intérêt / originalité</span></h2>
<p>Quand j’étais étudiant avec ma petite activité d’hébergement, surtout du fait qu’elle était à prix libre, j’ai eu le droit à 5 min d’antenne sur France Inter dans l’émission carnet de campagne. Ma boîte mail a explosé, mon site internet est « tombé » au bout d’une heure à 1500 visiteurs simultanés. Il ne comptait plus…</p>
<p>Le faire uniquement pour ceci serait malhonnête selon moi, mais ça permettra peut-être de faire basculer des indécis…</p>
<h2><span id="Certaines_difficultes">Certaines difficultés</span></h2>
<p>Il est très difficile (impossible ?) de pratiquer le prix libre avec des administrations publiques du fait de la réglementation. De même qu’un dialogue avec une grosse entreprise est complexe. Plus le décideur est proche de l’interlocuteur demandeur, plus ça a de chances de fonctionner. Bien sûr, si c’est la même personne, c’est encore plus facile, ça fait moins de gens à qui expliquer la chose. Dans ce type de cas, je ne cherche même pas, je donne un prix fixe.</p>
<h2><span id="Ca_marchenbsp_On_mange_a_prix_librenbsp"><strong>Ça marche ? On mange à prix libre ?</strong></span></h2>
<p>Pratiqué ainsi, le prix libre ne m’a que très rarement déçu. J’ai parfois des craintes, des peurs, mais je suis quasi toujours agréablement surpris de la générosité. De mon côté, certaines fois, le prix libre dépasse largement ce que j’aurais estimé juste de demander sur un prix fixe.</p>
<p>La nuance que je peux apporter, c’est qu’il y a certains domaines d’activités qui sont plus ou moins reconnus socialement et donc de fait sont plus ou moins rémunérateurs (et c’est parfois très injuste). Mais le prix libre et conscient peut aussi permettre à certaines activités peu reconnues/rémunératrices de s’en sortir un peu mieux. J’ai pour exemple mon amie qui est <a rel="noreferrer noopener" href="https://www.ladamequipique.fr/" data-type="URL" data-id="https://www.ladamequipique.fr/" target="_blank">couturière</a> (retouche) qui <a rel="noreferrer noopener" href="https://www.ladamequipique.fr/prix-libre/" data-type="URL" data-id="https://www.ladamequipique.fr/prix-libre/" target="_blank">pratique le prix libre et conscient</a>. Elle propose une fourchette de prix par simplicité (de multiple clients avec de petites sommes, si on doit passer 20 minutes a expliquer la démarche ça devient trop coûteux en temps…) en précisant la fourchette basse c’est « le prix pour une rémunération au SMIC » et la fourchette haute « permet de réaliser une marge plus ou moins importante selon le service (selon sa fréquence/selon le prix affiché chez des homologues, etc. » (oui le travail de couture fait parti de ces nombreux métiers indispensables, très peu valorisés financièrement rapport au temps passé – peut-être parce que la concurrence étrangère travaille pour trop peu d’argent/sans acquis sociaux…). La bonne surprise, c’est que le plus fréquemment, les gens lui donnent la fourchette haute, voir même se permettent de la dépasser. Ce dépassement est difficile à faire sur un prix fixe.</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>

+ 5
- 0
cache/2022/d9af1ba02055491fc25b6849b8fd65d0/index.md
File diff suppressed because it is too large
View File


+ 269
- 0
cache/2022/ed7544349c2bef8c7f1bfff3ab286fd6/index.html View File

@@ -0,0 +1,269 @@
<!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>It is important for free software to use free software infrastructure (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://drewdevault.com/2022/03/29/free-software-free-infrastructure.html">

<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>It is important for free software to use free software infrastructure</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://drewdevault.com/2022/03/29/free-software-free-infrastructure.html" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p><em>Disclaimer: I founded a project and a company that focuses on free software
infrastructure. I will elect not to name them in this post, and will only
recommend solutions I do not have a vested interest in.</em></p>
<p>Free and open source software (FOSS) projects need infrastructure. Somewhere to
host the code, to facilitate things like code review, end-user support, bug
tracking, marketing, and so on. A common example of this is the “forge”
platform: infrastructure which pitches itself as a one-stop shop for many of the
needs of FOSS projects in one place, such as code hosting and review, bug
tracking, discussions, and so on. Many projects will also reach for additional
platforms to provide other kinds of infrastructure: chat rooms, forums, social
media, and more.</p>
<p>Many of these needs have <abbr title="Projects which do not use a license compatible with the Free Software guidelines, i.e. non-FOSS.">non-free</abbr>,
proprietary solutions available. GitHub is a popular proprietary code forge, and
GitLab, the biggest competitor to GitHub, is partially non-free. Some projects
use Discord or Slack for chat rooms, Reddit as a forum, or Twitter and Facebook
for marketing, outreach, and support; all of these are non-free. In my opinion,
relying on these platforms to provide infrastructure for your FOSS project is a
mistake.</p>
<p>When your FOSS project chooses to use a non-free platform, you give it an
official vote of confidence on behalf of your project. In other words, you lend
some of your project’s credibility and legitimacy to the platforms you choose.
These platforms are defined by network effects, and your choice is an investment
in that network. I would question this investment in and of itself, the wisdom
of offering these platforms your confidence and legitimacy, but there’s a more
concerning consequence of this choice as well: an investment in a non-free
platform is also a <em>divestment</em> from the free alternatives.</p>
<p>Again, network effects are the main driver of success in these platforms. Large
commercial platforms have a lot of advantages in this respect: large marketing
budgets, lots of capital from investors, and the incumbency advantage. The
larger the incumbent platform, the more difficult the task of competing with it
becomes. Contrast this with free software platforms, which generally don’t have
the benefit of large amounts of investment or big marketing budgets. Moreover,
businesses are significantly more likely to play dirty to secure their foothold
than free software projects are. If your own FOSS projects compete with
proprietary commercial options, you should be very familiar with these
challenges.</p>
<p>FOSS platforms are at an inherent disadvantage, and your faith in them, or lack
thereof, carries a lot of weight. GitHub won’t lose sleep if your project
chooses to host its code somewhere else, but choosing <a href="https://codeberg.org">Codeberg</a>, for example,
means a lot to them. In effect, your choice matters disproportionately to the
free platforms: choosing GitHub hurts Codeberg much more than choosing Codeberg
hurts GitHub. And why should a project choose to use your offering over the
proprietary alternatives if you won’t extend them the same courtesy? FOSS
solidarity is important for uplifting the ecosystem as a whole.</p>
<p>However, for some projects, what ultimately matters to them has little to do
with the benefit of the ecosystem as a whole, but instead considers only the
potential for their project’s individual growth and popularity. Many projects
choose to prioritize access to the established audience that large commercial
platforms provide, in order to maximize their odds of becoming popular, and
enjoying some of the knock-on effects of that popularity, such as more
contributions.<sup id="fnref:1"></sup> Such projects would prefer to exacerbate the network
effects problem rather than risk some of its social capital on a less popular
platform.</p>
<p>To me, this is selfish and unethical outright, though you may have different
ethical standards. Unfortunately, arguments against most commercial platforms
for any reasonable ethical standard are available in abundance, but they tend to
be easily overcome by confirmation bias. Someone who may loudly object to the
practices of the US Immigration and Customs Enforcement agency, for example, can
quickly find some justification to continue using GitHub despite their
collaboration with them. If this example isn’t to your tastes, there are many
examples for each of many platforms. For projects that don’t want to move, these
are usually swept under the rug.<sup id="fnref:2"></sup></p>
<p>But, to be clear, I am not asking you to use inferior platforms for
philosophical or altruistic reasons. These are only one of many factors which
should contribute to your decision-making, and aptitude is another valid factor
to consider. That said, many FOSS platforms are, at least in my opinion,
functionally superior to their proprietary competition. Whether their
differences are better for your project’s unique needs is something I must leave
for you to research on your own, but most projects don’t bother with the
research at all. Rest assured: these projects are not ghettos living in the
shadow of their larger commercial counterparts, but exciting platforms in their
own right which offer many unique advantages.</p>
<p>What’s more, if you need them to do something differently to better suit your
project’s needs, you are empowered to improve them. You’re not subservient to
the whims of the commercial entity who is responsible for the code, waiting for
them to prioritize the issue or even to care about it in the first place. If a
problem is important to you, that’s enough for you to get it fixed on a FOSS
platform. You might not think you have the time or expertise to do so (though
maybe one of your collaborators does), but more importantly, this establishes a
mentality of collective ownership and responsibility over all free software as a
whole — popularize this philosophy and it could just as easily be you
receiving a contribution in a similar manner tomorrow.</p>
<p>In short, choosing non-free platforms is an individualist, short-term investment
which prioritizes your project’s apparent access to popularity over the success
of the FOSS ecosystem as a whole. On the other hand, choosing FOSS platforms is
a collectivist investment in the long-term success of the FOSS ecosystem as a
whole, driving its overall growth. Your choice matters. You can help the FOSS
ecosystem by choosing FOSS platforms, or you can hurt the FOSS ecosystem by
choosing non-free platforms. Please choose carefully.</p>
<p>Here are some recommendations for free software tools that facilitate common
needs for free software projects:</p>

<p>* Self-hosted only<br>
† Partially non-free, recommended only if no other solutions are suitable</p>
<p>P.S. If your project is already established on non-free platforms, the easiest
time to revisit this choice is right now. It will only ever get more difficult
to move as your project grows and gets further established on proprietary
platforms. Please consider moving sooner rather than later.</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>

+ 102
- 0
cache/2022/ed7544349c2bef8c7f1bfff3ab286fd6/index.md View File

@@ -0,0 +1,102 @@
title: It is important for free software to use free software infrastructure
url: https://drewdevault.com/2022/03/29/free-software-free-infrastructure.html
hash_url: ed7544349c2bef8c7f1bfff3ab286fd6

<p><em>Disclaimer: I founded a project and a company that focuses on free software
infrastructure. I will elect not to name them in this post, and will only
recommend solutions I do not have a vested interest in.</em></p>
<p>Free and open source software (FOSS) projects need infrastructure. Somewhere to
host the code, to facilitate things like code review, end-user support, bug
tracking, marketing, and so on. A common example of this is the “forge”
platform: infrastructure which pitches itself as a one-stop shop for many of the
needs of FOSS projects in one place, such as code hosting and review, bug
tracking, discussions, and so on. Many projects will also reach for additional
platforms to provide other kinds of infrastructure: chat rooms, forums, social
media, and more.</p>
<p>Many of these needs have <abbr title="Projects which do not use a license compatible with the Free Software guidelines, i.e. non-FOSS.">non-free</abbr>,
proprietary solutions available. GitHub is a popular proprietary code forge, and
GitLab, the biggest competitor to GitHub, is partially non-free. Some projects
use Discord or Slack for chat rooms, Reddit as a forum, or Twitter and Facebook
for marketing, outreach, and support; all of these are non-free. In my opinion,
relying on these platforms to provide infrastructure for your FOSS project is a
mistake.</p>
<p>When your FOSS project chooses to use a non-free platform, you give it an
official vote of confidence on behalf of your project. In other words, you lend
some of your project’s credibility and legitimacy to the platforms you choose.
These platforms are defined by network effects, and your choice is an investment
in that network. I would question this investment in and of itself, the wisdom
of offering these platforms your confidence and legitimacy, but there’s a more
concerning consequence of this choice as well: an investment in a non-free
platform is also a <em>divestment</em> from the free alternatives.</p>
<p>Again, network effects are the main driver of success in these platforms. Large
commercial platforms have a lot of advantages in this respect: large marketing
budgets, lots of capital from investors, and the incumbency advantage. The
larger the incumbent platform, the more difficult the task of competing with it
becomes. Contrast this with free software platforms, which generally don’t have
the benefit of large amounts of investment or big marketing budgets. Moreover,
businesses are significantly more likely to play dirty to secure their foothold
than free software projects are. If your own FOSS projects compete with
proprietary commercial options, you should be very familiar with these
challenges.</p>
<p>FOSS platforms are at an inherent disadvantage, and your faith in them, or lack
thereof, carries a lot of weight. GitHub won’t lose sleep if your project
chooses to host its code somewhere else, but choosing <a href="https://codeberg.org">Codeberg</a>, for example,
means a lot to them. In effect, your choice matters disproportionately to the
free platforms: choosing GitHub hurts Codeberg much more than choosing Codeberg
hurts GitHub. And why should a project choose to use your offering over the
proprietary alternatives if you won’t extend them the same courtesy? FOSS
solidarity is important for uplifting the ecosystem as a whole.</p>
<p>However, for some projects, what ultimately matters to them has little to do
with the benefit of the ecosystem as a whole, but instead considers only the
potential for their project’s individual growth and popularity. Many projects
choose to prioritize access to the established audience that large commercial
platforms provide, in order to maximize their odds of becoming popular, and
enjoying some of the knock-on effects of that popularity, such as more
contributions.<sup id="fnref:1"></sup> Such projects would prefer to exacerbate the network
effects problem rather than risk some of its social capital on a less popular
platform.</p>
<p>To me, this is selfish and unethical outright, though you may have different
ethical standards. Unfortunately, arguments against most commercial platforms
for any reasonable ethical standard are available in abundance, but they tend to
be easily overcome by confirmation bias. Someone who may loudly object to the
practices of the US Immigration and Customs Enforcement agency, for example, can
quickly find some justification to continue using GitHub despite their
collaboration with them. If this example isn’t to your tastes, there are many
examples for each of many platforms. For projects that don’t want to move, these
are usually swept under the rug.<sup id="fnref:2"></sup></p>
<p>But, to be clear, I am not asking you to use inferior platforms for
philosophical or altruistic reasons. These are only one of many factors which
should contribute to your decision-making, and aptitude is another valid factor
to consider. That said, many FOSS platforms are, at least in my opinion,
functionally superior to their proprietary competition. Whether their
differences are better for your project’s unique needs is something I must leave
for you to research on your own, but most projects don’t bother with the
research at all. Rest assured: these projects are not ghettos living in the
shadow of their larger commercial counterparts, but exciting platforms in their
own right which offer many unique advantages.</p>
<p>What’s more, if you need them to do something differently to better suit your
project’s needs, you are empowered to improve them. You’re not subservient to
the whims of the commercial entity who is responsible for the code, waiting for
them to prioritize the issue or even to care about it in the first place. If a
problem is important to you, that’s enough for you to get it fixed on a FOSS
platform. You might not think you have the time or expertise to do so (though
maybe one of your collaborators does), but more importantly, this establishes a
mentality of collective ownership and responsibility over all free software as a
whole — popularize this philosophy and it could just as easily be you
receiving a contribution in a similar manner tomorrow.</p>
<p>In short, choosing non-free platforms is an individualist, short-term investment
which prioritizes your project’s apparent access to popularity over the success
of the FOSS ecosystem as a whole. On the other hand, choosing FOSS platforms is
a collectivist investment in the long-term success of the FOSS ecosystem as a
whole, driving its overall growth. Your choice matters. You can help the FOSS
ecosystem by choosing FOSS platforms, or you can hurt the FOSS ecosystem by
choosing non-free platforms. Please choose carefully.</p>
<p>Here are some recommendations for free software tools that facilitate common
needs for free software projects:</p>

<p>* Self-hosted only<br>
† Partially non-free, recommended only if no other solutions are suitable</p>
<p>P.S. If your project is already established on non-free platforms, the easiest
time to revisit this choice is right now. It will only ever get more difficult
to move as your project grows and gets further established on proprietary
platforms. Please consider moving sooner rather than later.</p>

+ 24
- 0
cache/2022/index.html View File

@@ -67,14 +67,20 @@
<main>
<ul>
<li><a href="/david/cache/2022/3e8bb1b63246d6f97316864569492382/" title="Accès à l’article dans le cache local : Technical Solutions Poorly Solve Social Problems">Technical Solutions Poorly Solve Social Problems</a> (<a href="https://christine.website/blog/social-quandry-devops-2022-03-17" title="Accès à l’article original distant : Technical Solutions Poorly Solve Social Problems">original</a>)</li>
<li><a href="/david/cache/2022/99a44a14a9d140bd39686955a78e5e9f/" title="Accès à l’article dans le cache local : What is the Web?">What is the Web?</a> (<a href="https://blog.jim-nielsen.com/2022/what-is-the-web/" title="Accès à l’article original distant : What is the Web?">original</a>)</li>
<li><a href="/david/cache/2022/a863c20d0cb9722df74219009e8365a3/" title="Accès à l’article dans le cache local : Jakarta’s Transit Miracle">Jakarta’s Transit Miracle</a> (<a href="https://infiniteblock.substack.com/p/jakartas-transit-miracle" title="Accès à l’article original distant : Jakarta’s Transit Miracle">original</a>)</li>
<li><a href="/david/cache/2022/389205e96cefd5e4633c70f22d029e1b/" title="Accès à l’article dans le cache local : Un hacker pirate le vol d'un avion depuis un siège passager">Un hacker pirate le vol d'un avion depuis un siège passager</a> (<a href="https://www.silicon.fr/hacker-modifie-vol-dun-avion-systeme-de-divertissement-116431.html" title="Accès à l’article original distant : Un hacker pirate le vol d'un avion depuis un siège passager">original</a>)</li>
<li><a href="/david/cache/2022/10bfffb87f475e23c0dd0a30527b9750/" title="Accès à l’article dans le cache local : How a Wikipedia volunteer editor became a very vocal Web3 critic">How a Wikipedia volunteer editor became a very vocal Web3 critic</a> (<a href="https://www.fastcompany.com/90733574/how-a-wikipedia-engineer-became-one-of-the-loudest-web3-skeptics" title="Accès à l’article original distant : How a Wikipedia volunteer editor became a very vocal Web3 critic">original</a>)</li>
<li><a href="/david/cache/2022/d4ae86dd75af3abc8b65953e7d3ca832/" title="Accès à l’article dans le cache local : ★ Children of the Hyperlink">★ Children of the Hyperlink</a> (<a href="https://buttondown.email/robinrendle/archive/children-of-the-hyperlink/" title="Accès à l’article original distant : ★ Children of the Hyperlink">original</a>)</li>
<li><a href="/david/cache/2022/4bf828ef0ce7191d048d0c510a3c3e0c/" title="Accès à l’article dans le cache local : ☕️ Journal : Marges">☕️ Journal : Marges</a> (<a href="https://thom4.net/2022/03/23/marges/" title="Accès à l’article original distant : ☕️ Journal : Marges">original</a>)</li>
<li><a href="/david/cache/2022/91078de938da3b77cff57427da41dc11/" title="Accès à l’article dans le cache local : How Websites Die">How Websites Die</a> (<a href="https://notebook.wesleyac.com/how-websites-die/" title="Accès à l’article original distant : How Websites Die">original</a>)</li>
<li><a href="/david/cache/2022/73c1cc8ed70f3b78bb9f8d2f108b7754/" title="Accès à l’article dans le cache local : ⭕️ Signals • Buttondown">⭕️ Signals • Buttondown</a> (<a href="https://buttondown.email/robinrendle/archive/signals/" title="Accès à l’article original distant : ⭕️ Signals • Buttondown">original</a>)</li>
@@ -85,6 +91,8 @@
<li><a href="/david/cache/2022/0a53d8dedc371884d16f45bcb349b418/" title="Accès à l’article dans le cache local : BALLAST • QUE FAIRE ?">BALLAST • QUE FAIRE ?</a> (<a href="https://www.revue-ballast.fr/que-faire/" title="Accès à l’article original distant : BALLAST • QUE FAIRE ?">original</a>)</li>
<li><a href="/david/cache/2022/d97914db7d2e525edc27669adbc0f917/" title="Accès à l’article dans le cache local : A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly">A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly</a> (<a href="https://endler.dev/2019/tinysearch/" title="Accès à l’article original distant : A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly">original</a>)</li>
<li><a href="/david/cache/2022/65e0c481f692260299c53e9713339f53/" title="Accès à l’article dans le cache local : Ce dont nous avons (vraiment) besoin">Ce dont nous avons (vraiment) besoin</a> (<a href="https://www.monde-diplomatique.fr/2017/02/KEUCHEYAN/57134" title="Accès à l’article original distant : Ce dont nous avons (vraiment) besoin">original</a>)</li>
<li><a href="/david/cache/2022/b17f8ac80615c86cade89dd81c8aa50b/" title="Accès à l’article dans le cache local : Server-Sent Events: the alternative to WebSockets you should be using">Server-Sent Events: the alternative to WebSockets you should be using</a> (<a href="https://germano.dev/sse-websockets/#comments" title="Accès à l’article original distant : Server-Sent Events: the alternative to WebSockets you should be using">original</a>)</li>
@@ -93,10 +101,14 @@
<li><a href="/david/cache/2022/622620656409b4f687cab890288a0a01/" title="Accès à l’article dans le cache local : Who can be the Netflix of ghost kitchens?">Who can be the Netflix of ghost kitchens?</a> (<a href="https://interconnected.org/home/2022/01/24/meme_meals" title="Accès à l’article original distant : Who can be the Netflix of ghost kitchens?">original</a>)</li>
<li><a href="/david/cache/2022/ed7544349c2bef8c7f1bfff3ab286fd6/" title="Accès à l’article dans le cache local : It is important for free software to use free software infrastructure">It is important for free software to use free software infrastructure</a> (<a href="https://drewdevault.com/2022/03/29/free-software-free-infrastructure.html" title="Accès à l’article original distant : It is important for free software to use free software infrastructure">original</a>)</li>
<li><a href="/david/cache/2022/987e2e450e3e88d0d6d18ec6e6a44b95/" title="Accès à l’article dans le cache local : Habiter sans posséder, tel est l’antidote">Habiter sans posséder, tel est l’antidote</a> (<a href="https://revoirleslucioles.org/habiter-sans-posseder-tel-est-lantidote/" title="Accès à l’article original distant : Habiter sans posséder, tel est l’antidote">original</a>)</li>
<li><a href="/david/cache/2022/cb9e84580f4cb201995be13a7df2117b/" title="Accès à l’article dans le cache local : Gemini is Solutionism at its Worst">Gemini is Solutionism at its Worst</a> (<a href="https://マリウス.com/gemini-is-solutionism-at-its-worst/" title="Accès à l’article original distant : Gemini is Solutionism at its Worst">original</a>)</li>
<li><a href="/david/cache/2022/09da2c716e8bb15e5d22ee071cf39358/" title="Accès à l’article dans le cache local : How to create a search page for a static website with vanilla JS">How to create a search page for a static website with vanilla JS</a> (<a href="https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/" title="Accès à l’article original distant : How to create a search page for a static website with vanilla JS">original</a>)</li>
<li><a href="/david/cache/2022/8a9c9c7aa6a17b8203e2ee289a5e2ffa/" title="Accès à l’article dans le cache local : Coremuneration - Movilab.org">Coremuneration - Movilab.org</a> (<a href="https://movilab.org/wiki/Coremuneration" title="Accès à l’article original distant : Coremuneration - Movilab.org">original</a>)</li>
<li><a href="/david/cache/2022/9ad9f5ea367dbd74e4aeeb8471747247/" title="Accès à l’article dans le cache local : Make it boring - jlwagner.net">Make it boring - jlwagner.net</a> (<a href="https://jlwagner.net/blog/make-it-boring/" title="Accès à l’article original distant : Make it boring - jlwagner.net">original</a>)</li>
@@ -121,6 +133,8 @@
<li><a href="/david/cache/2022/891705d1555d09a941fd1f7685de9370/" title="Accès à l’article dans le cache local : dataklasses: A different spin on dataclasses.">dataklasses: A different spin on dataclasses.</a> (<a href="https://github.com/dabeaz/dataklasses#questions-and-answers" title="Accès à l’article original distant : dataklasses: A different spin on dataclasses.">original</a>)</li>
<li><a href="/david/cache/2022/2f3ed5cb927427fb834b4a9d657592be/" title="Accès à l’article dans le cache local : Ajout d’un module de recherche pour Hugo">Ajout d’un module de recherche pour Hugo</a> (<a href="https://lord.re/posts/206-recherche-pour-un-blog-statique/" title="Accès à l’article original distant : Ajout d’un module de recherche pour Hugo">original</a>)</li>
<li><a href="/david/cache/2022/0d024905896d89f8bd499e2a6170b59e/" title="Accès à l’article dans le cache local : What’s Really Going On Inside Your node_modules Folder?">What’s Really Going On Inside Your node_modules Folder?</a> (<a href="https://socket.dev/blog/inside-node-modules" title="Accès à l’article original distant : What’s Really Going On Inside Your node_modules Folder?">original</a>)</li>
<li><a href="/david/cache/2022/bfc65237a09e49939b31ba887d7e3fc8/" title="Accès à l’article dans le cache local : Open-source - Open Time">Open-source - Open Time</a> (<a href="https://open-time.net/post/2022/03/23/Open-source" title="Accès à l’article original distant : Open-source - Open Time">original</a>)</li>
@@ -131,6 +145,8 @@
<li><a href="/david/cache/2022/0c0894907925eae954987d98c9e8136b/" title="Accès à l’article dans le cache local : Why I Quit Tech and Became a Therapist">Why I Quit Tech and Became a Therapist</a> (<a href="http://glench.com/WhyIQuitTechAndBecameATherapist/" title="Accès à l’article original distant : Why I Quit Tech and Became a Therapist">original</a>)</li>
<li><a href="/david/cache/2022/5eb0016b355ac4b358be367fe64f4c84/" title="Accès à l’article dans le cache local : Mourning Loss of a Team Member as a Remote Team">Mourning Loss of a Team Member as a Remote Team</a> (<a href="https://www.sofuckingagile.com/blog/mourning-loss-as-a-remote-team" title="Accès à l’article original distant : Mourning Loss of a Team Member as a Remote Team">original</a>)</li>
<li><a href="/david/cache/2022/053b5d423df20fa4e7978174d91d41bb/" title="Accès à l’article dans le cache local : Making Gemini Easy">Making Gemini Easy</a> (<a href="https://proxy.vulpes.one/gemini/tilde.team/~tomasino/journal/20211103-making-gemini-easy.gmi" title="Accès à l’article original distant : Making Gemini Easy">original</a>)</li>
<li><a href="/david/cache/2022/7591561f82b6ec5b32ead9df89a11c15/" title="Accès à l’article dans le cache local : Pourquoi Poutine a déjà perdu la guerre">Pourquoi Poutine a déjà perdu la guerre</a> (<a href="https://legrandcontinent.eu/fr/2022/02/27/pourquoi-poutine-a-deja-perdu-la-guerre/" title="Accès à l’article original distant : Pourquoi Poutine a déjà perdu la guerre">original</a>)</li>
@@ -145,6 +161,8 @@
<li><a href="/david/cache/2022/580f9e17e55d43379850d613f51cf3a2/" title="Accès à l’article dans le cache local : Apple et la convivialité">Apple et la convivialité</a> (<a href="https://louisderrac.com/2022/03/23/apple-et-la-convialite/" title="Accès à l’article original distant : Apple et la convivialité">original</a>)</li>
<li><a href="/david/cache/2022/2c67b87e1b880952bb277fc429cb8bf5/" title="Accès à l’article dans le cache local : How to update the URL of a page without causing a reload using vanilla JavaScript">How to update the URL of a page without causing a reload using vanilla JavaScript</a> (<a href="https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/" title="Accès à l’article original distant : How to update the URL of a page without causing a reload using vanilla JavaScript">original</a>)</li>
<li><a href="/david/cache/2022/86a502931562f5a88120be5ae903b67a/" title="Accès à l’article dans le cache local : An African view of what’s happening in Europe">An African view of what’s happening in Europe</a> (<a href="https://www.opendemocracy.net/en/5050/an-african-view-of-whats-happening-in-europe/" title="Accès à l’article original distant : An African view of what’s happening in Europe">original</a>)</li>
<li><a href="/david/cache/2022/21c1a3b62ce222105d72ada4802bdd4e/" title="Accès à l’article dans le cache local : Wrap Up and Q&amp;A - Jacob Kaplan-Moss">Wrap Up and Q&amp;A - Jacob Kaplan-Moss</a> (<a href="https://jacobian.org/2022/jan/6/wst-wrap-up/" title="Accès à l’article original distant : Wrap Up and Q&amp;A - Jacob Kaplan-Moss">original</a>)</li>
@@ -153,6 +171,10 @@
<li><a href="/david/cache/2022/20648a9bc173f75256ae9d5f196fd913/" title="Accès à l’article dans le cache local : The happiest number I've heard in ages">The happiest number I've heard in ages</a> (<a href="https://billmckibben.substack.com/p/the-happiest-number-ive-heard-in" title="Accès à l’article original distant : The happiest number I've heard in ages">original</a>)</li>
<li><a href="/david/cache/2022/d9af1ba02055491fc25b6849b8fd65d0/" title="Accès à l’article dans le cache local : Ma pratique du prix libre et conscient">Ma pratique du prix libre et conscient</a> (<a href="https://david.mercereau.info/ma-pratique-du-prix-libre-et-conscient/" title="Accès à l’article original distant : Ma pratique du prix libre et conscient">original</a>)</li>
<li><a href="/david/cache/2022/cf85372fcb8da232d3fb8d95a88bc8fe/" title="Accès à l’article dans le cache local : Stop making the Ukraine war about you">Stop making the Ukraine war about you</a> (<a href="https://www.dazeddigital.com/politics/article/55563/1/stop-making-the-ukraine-war-about-you" title="Accès à l’article original distant : Stop making the Ukraine war about you">original</a>)</li>
<li><a href="/david/cache/2022/f57abf8bb9e96e5cb5cfe845d76729f5/" title="Accès à l’article dans le cache local : “Open Source” is Broken">“Open Source” is Broken</a> (<a href="https://christine.website/blog/open-source-broken-2021-12-11" title="Accès à l’article original distant : “Open Source” is Broken">original</a>)</li>
<li><a href="/david/cache/2022/69acddf6a1f953e130ab2b36960568b7/" title="Accès à l’article dans le cache local : Le disque vinyle : débunkage">Le disque vinyle : débunkage</a> (<a href="https://blog.cybergrunge.dev/le-disque-vinyle-debunkage" title="Accès à l’article original distant : Le disque vinyle : débunkage">original</a>)</li>
@@ -191,6 +213,8 @@
<li><a href="/david/cache/2022/48c2a5401b4445c412167f0ae1280ad8/" title="Accès à l’article dans le cache local : Cooklang - Managing Recipes in Git">Cooklang - Managing Recipes in Git</a> (<a href="https://briansunter.com/blog/cooklang/" title="Accès à l’article original distant : Cooklang - Managing Recipes in Git">original</a>)</li>
<li><a href="/david/cache/2022/c7ebf32ee18c4f44c452f864729a21a8/" title="Accès à l’article dans le cache local : The drone operators who halted Russian convoy headed for Kyiv">The drone operators who halted Russian convoy headed for Kyiv</a> (<a href="https://www.theguardian.com/world/2022/mar/28/the-drone-operators-who-halted-the-russian-armoured-vehicles-heading-for-kyiv" title="Accès à l’article original distant : The drone operators who halted Russian convoy headed for Kyiv">original</a>)</li>
<li><a href="/david/cache/2022/0d734a1e83d3188bb008a057aadd4a74/" title="Accès à l’article dans le cache local : ☕️ Journal : Faire équipe">☕️ Journal : Faire équipe</a> (<a href="https://oncletom.io/2022/01/30/faire-equipe/" title="Accès à l’article original distant : ☕️ Journal : Faire équipe">original</a>)</li>
<li><a href="/david/cache/2022/571d5d3f9d63d9ec4a8107e5abd15941/" title="Accès à l’article dans le cache local : Why not everything I do is “Open” or “Free”">Why not everything I do is “Open” or “Free”</a> (<a href="https://overengineer.dev/blog/2021/12/12/why-not-everything-i-do-is-open-or-free.html" title="Accès à l’article original distant : Why not everything I do is “Open” or “Free”">original</a>)</li>

Loading…
Cancel
Save