Nicolas Hoizey - Articles 2023-05-19T17:47:56Z https://nicolas-hoizey.com/articles/ Nicolas Hoizey nicolas@hoizey.com Eleventy v2.0.1 HTML and CSS based View Transitions are coming 2023-05-19T17:47:56Z https://nicolas-hoizey.com/articles/2023/05/19/html-and-css-based-view-transitions-are-coming/ <div class="lead"> <p>While same-document View Transitions have now been <a href="https://caniuse.com/?search=ViewTransition">available for a while</a> in Chromium browsers for Single Page Applications (SPA), they were requiring the use of a JavaScript API. Chrome Canary now allows us to develop and test View Transitions with HTML and CSS only, obviously targeting Multiple Pages Applications (aka Web sites đŸ€·â€â™‚ïž).</p> </div> <div class="heading-wrapper"> <h2 id="let-s-experiment" tabindex="-1">Let's experiment!</h2> </div> <p>After following <a href="https://developer.chrome.com/docs/web-platform/view-transitions/">Jake Archibald's work</a> for many months now, and sharing <a href="https://nicolas-hoizey.com/links/?tags=View%20Transitions">a few links about View Transitions</a>, I wanted to try them, and decided <a href="https://nicolas-hoizey.photo/">my photography site</a> would be a good playground.</p> <p>Here's what I got with just <a href="https://github.com/nhoizey/nicolas-hoizey.photo/blob/0ebfd123ab203c330dd24dc1abf2f0a068390b4e/src/_layouts/base.njk#L41">a <code>&lt;meta&gt;</code> tag</a> and <a href="https://github.com/nhoizey/nicolas-hoizey.photo/blob/0ebfd123ab203c330dd24dc1abf2f0a068390b4e/assets/sass/_view-transitions.scss">a few CSS rules</a>:</p> <p><a href="https://youtu.be/Z_MG97DzNPs">https://youtu.be/Z_MG97DzNPs</a></p> <p>Thanks Dave Rupert for the <a href="https://nicolas-hoizey.com/links/2023/05/19/getting-started-with-view-transitions-on-multi-page-apps/">very simple View Transitions tutorial</a>!</p> <p>There are a few improvements required for when we transition from a small and lightweight thumbnail to a very large and heavy photo (still no <a href="https://nicolas-hoizey.com/tags/jpeg-xl/">JPEG-XL</a>
), so I tried to add a Low Quality Image Placeholder (I usually hate them
 😞) to limit the issue on slow networks. But I guess there should be a better solution if I could keep the thumbnail while the large image loads.</p> <div class="heading-wrapper"> <h2 id="ok-for-chrome-canary-but-elsewhere" tabindex="-1">Ok for Chrome Canary, but elsewhere?</h2> </div> <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API">documentation for the View Transitions API on MDN</a> is already available, but focused on SPA.</p> <p>Noam Rosenthal is currently leading the <a href="https://github.com/WICG/view-transitions/pull/208">creation of a new explainer for &quot;cross-document navigations&quot; (MPA)</a>, a welcome step towards standardisation, and maybe implementation in other browsers.</p> <p>There are also <a href="https://github.com/w3c/csswg-drafts/labels/css-view-transitions-2">a lot of open issues in the CSS Working Group GitHub repository</a> with ideas and questions.</p> <p>Whatever the standardisation and cross-browser implementation status, a really nice thing about View Transitions is that they have been designed as a progressive enhancement, so you can use them right now, even if support is currently low (in terms of browser diversity).</p> <p>I the mean time, I can only thank Jake Archibald A LOT for this nice improvement of the user experience! 🙏</p> Running CSS animations only if both the device and the user allow it 2023-04-07T17:49:14Z https://nicolas-hoizey.com/articles/2023/04/07/running-css-animations-only-if-both-the-device-and-the-user-allow-it/ <div class="lead"> <p>Thanks to Chrome release notes, <a href="https://nicolas-hoizey.com/notes/2023/04/07/1/">I discovered today</a> that there is <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/update-frequency">an <code>update</code> media feature</a> which accepts values <code>fast</code>, <code>slow</code> and <code>print</code>, to set styles depending on the ability of the device to update the rendering and the speed of it.</p> <p>As I'm already respecting the user's preference with the <code>prefers-reduced-motion</code> media feature, I wondered how I could progressively enhance this with the new media feature.</p> </div> <div class="heading-wrapper"> <h2 id="testing-media-feature-support-without-supports" tabindex="-1">&quot;Testing&quot; media feature support without <code>@supports</code></h2> </div> <p><a href="https://front-end.social/@AmeliaBR">Amelia Bellamy-Royds</a> had <a href="https://front-end.social/@AmeliaBR/110158330793667431">the answer with a clever trick</a>. Thanks Amelia! 🙏</p> <p>This is how you can apply styles only if a feature is supported by the browser:</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">thing</span><span class="token punctuation">:</span> one<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token keyword">not</span> <span class="token punctuation">(</span><span class="token property">thing</span><span class="token punctuation">:</span> one<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> 
 <span class="token punctuation">}</span></code></pre> <p>Indeed:</p> <ul> <li>either the browser doesn't understand <code>thing: one</code> and ignores both media queries</li> <li>or the browser understands <code>thing: one</code> and this is either <code>true</code> or <code>false</code>, so combining both matches all supporting browsers/contexts</li> </ul> <div class="heading-wrapper"> <h2 id="running-css-animations-only-if-both-the-device-either-update-fast-not-supported-or-true-and-the-user-allow-it-prefers-reduced-motion-no-preference" tabindex="-1">Running CSS animations only if both the device (either <code>update: fast</code> not supported or <code>true</code>) and the user allow it (<code>prefers-reduced-motion: no-preference</code>)</h2> </div> <p>Combining this trick with my already existing media queries for the <code>prefers-reduced-motion</code> media feature requires a bit of code, but it's manageable.</p> <p>Here's the code I got for the Ken Burns animations running on <a href="https://nicolas-hoizey.photo/">my photography site</a> (with non relevant selectors cleaned up):</p> <pre class="language-css"><code class="language-css">// Without @​media update support // Enable animations if no reduced motion preference <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">img</span> <span class="token punctuation">{</span> <span class="token property">animation-play-state</span><span class="token punctuation">:</span> running<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> // With @​media update support <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">update</span><span class="token punctuation">:</span> fast<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token keyword">not</span> <span class="token punctuation">(</span><span class="token property">update</span><span class="token punctuation">:</span> fast<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> // If screen update is fast <span class="token punctuation">(</span>neither slow nor print<span class="token punctuation">)</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">update</span><span class="token punctuation">:</span> fast<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> // Enable animations if no reduced motion preference <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">img</span> <span class="token punctuation">{</span> <span class="token property">animation-play-state</span><span class="token punctuation">:</span> running<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> // If screen update is NOT fast <span class="token punctuation">(</span>either slow or print<span class="token punctuation">)</span> // Disable animations <span class="token atrule"><span class="token rule">@media</span> <span class="token keyword">not</span> <span class="token punctuation">(</span><span class="token property">update</span><span class="token punctuation">:</span> fast<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">img</span> <span class="token punctuation">{</span> <span class="token property">animation-play-state</span><span class="token punctuation">:</span> paused<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>Being able to nest media queries is great!</p> <div class="heading-wrapper"> <h2 id="later-update-a-simpler-solution-thanks-to-amelia" tabindex="-1">Later update: a simpler solution thanks to Amelia</h2> </div> <p>As <a href="https://front-end.social/@AmeliaBR/110160694917595587">Amelia noticed when I shared this</a> (I ❀ her comment “both awesome &amp; awful” 😅), the code can be much simpler, without media queries inception. 😅</p> <p>Here's what <a href="https://front-end.social/@AmeliaBR/110160702621003752">Amelia suggests to write</a>, and I agree it's much better:</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">img</span> <span class="token punctuation">{</span> <span class="token property">animation-play-state</span><span class="token punctuation">:</span> running<span class="token punctuation">;</span> <span class="token comment">/* turn animations on if user doesn't mind */</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token keyword">not</span> <span class="token punctuation">(</span><span class="token property">update</span><span class="token punctuation">:</span> fast<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">img</span> <span class="token punctuation">{</span> <span class="token property">animation-play-state</span><span class="token punctuation">:</span> paused<span class="token punctuation">;</span> <span class="token comment">/* except, turn them off again if the browser can't draw them effectively anyway */</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>I feel so lucky to regularly get great feedback from people with such expertise as Amelia, on many topics. <a href="https://nicolas-hoizey.com/links/2023/04/07/how-tweetbot-died-and-lived-again/">Once with Twitter, I now get this with Mastodon</a>, and it feels even better.</p> <p>Now
 how can I test the different permutations of <code>update</code> support or not, and actual value? đŸ€”</p> How I built my own excerpt for Markdown content in Eleventy 2023-03-30T23:40:32Z https://nicolas-hoizey.com/articles/2023/03/31/how-i-built-my-own-excerpt-for-markdown-content-in-eleventy/ <div class="lead"> <p>I was not really happy with <a href="https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content">Eleventy's native excerpt solution</a> requiring just a separator and having the excerpt content preserved in the content, without any way to style it differently. So I tried different alternatives, and settled on a solution with some Markdown-it plugins and a bunch of regexes.</p> </div> <p>To be able to style the content lead whatever it contains, I'm using the great and simple <a href="https://github.com/GerHobbelt/markdown-it-container#readme"><code>markdown-it-container</code></a> plugin for Markdown-it, with a <code>lead</code> container.</p> <p>For exemple, I can write this in the begining of my Markdown file, after the YAML Front Matter:</p> <pre class="language-markdown"><code class="language-markdown">::: lead This paragraph is in the lead. This other paragraph is also in the lead. ::: This is no more part of the content lead
</code></pre> <p>With this really simple syntax, I can put whatever I want in the lead and style it in the content page.</p> <p>And I can also extract it for the excerpt!</p> <p>I could have used a Nunjucks filter, as I previously did, but it means the excerpt for the same content would have been computed multiple times (unless I did some memoization) for different content listings in the homepage, a category page, archives pages, Atom and JSON feeds, Algolia index, etc.</p> <p>Fortunately, Eleventy provides the <code>eleventyConfig.setFrontMatterParsingOptions()</code> function which allows passing options to <a href="https://github.com/jonschlinkert/gray-matter#readme"><code>gray-matter</code></a>, the npm package it relies on to parse front matter.</p> <p>This is the function that allows setting a custom separator for the default excerpt feature with the <a href="https://github.com/jonschlinkert/gray-matter#optionsexcerpt_separator"><code>excerpt_separator</code></a> option, <a href="https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content">as shown in Eleventy documentation</a>, but in addition to the simple <code>true</code> boolean shown here, the <code>excerpt</code> option can be a function, which gets the Markdown content and options as parameters.</p> <p>Here's the function I built to generate my own excerpt:</p> <pre class="language-javascript"><code class="language-javascript"> <span class="token keyword">const</span> markdownItPlainText <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'markdown-it-plain-text'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> excerptMd <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">markdownIt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>markdownItPlainText<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">grayMatterExcerpt</span><span class="token punctuation">(</span><span class="token parameter">file<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> regex <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^.*::: lead(((?!(:::)).|\n)+):::.*$</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">;</span> <span class="token keyword">let</span> excerpt <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">;</span> <span class="token keyword">let</span> leadFound <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>leadMatches <span class="token operator">=</span> regex<span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>file<span class="token punctuation">.</span>content<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> lead <span class="token operator">=</span> leadMatches<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span> leadFound <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> excerptMd<span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span>lead<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> excerptMd<span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span>file<span class="token punctuation">.</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> excerpt <span class="token operator">=</span> excerptMd<span class="token punctuation">.</span>plainText <span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">{%(((?!(%})).|\n)+)%}</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token comment">// remove short codes</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">{{(((?!(}})).|\n)+)}}</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token comment">// remove nunjucks variables</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">{#(((?!(#})).|\n)+)#}</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token comment">// remove nunjucks comments</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">&lt;style>(((?!(&lt;\/style>)).|\n)+)&lt;\/style></span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token comment">// remove inline CSS</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">&lt;script type="application\/ld\+json">(((?!(&lt;\/script>)).|\n)+)&lt;\/script></span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span> <span class="token punctuation">)</span> <span class="token comment">// remove JSON+LD</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">(&lt;\/h[1-6]>)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">'. $1'</span><span class="token punctuation">)</span> <span class="token comment">// add a dot at the end of headings</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">&lt;\/?([a-z][a-z0-9]*)\b[^>]*>|&lt;!--[\s\S]*?--></span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token comment">// remove HTML tags</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">(\[\^[^\]]+\])</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token comment">// remove Markdown footnotes</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\[([^\]]+)\]\(\)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">'$1'</span><span class="token punctuation">)</span> <span class="token comment">// remove Markdown links without URL (from {% link_to %} for example)</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex"> +(\.|,)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">'$1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// remove space before punctuation</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>leadFound <span class="token operator">&amp;&amp;</span> excerpt<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">150</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Keep only 145 characters and an ellipsis if there was no declared lead</span> excerpt <span class="token operator">=</span> excerpt<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^(.{145}[^\s]*).*</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">'$1'</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">'
'</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> file<span class="token punctuation">.</span>excerpt <span class="token operator">=</span> excerpt<span class="token punctuation">;</span> <span class="token punctuation">}</span> eleventyConfig<span class="token punctuation">.</span><span class="token function">setFrontMatterParsingOptions</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">excerpt</span><span class="token operator">:</span> grayMatterExcerpt<span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> </code></pre> <p>It first looks for a <code>::: lead</code> container to use directly, or takes the full content.</p> <p>It then parses the result with Markdown-it and the <a href="https://github.com/wavesheep/markdown-it-plain-text#readme"><code>markdown-it-plain-text</code></a> plugin, and then removes some parts that are really not useful in an excerpt: Nunjucks short codes/variables/comments, inline CSS rules, JSON/LD metadata, remaining HTML tags, footnotes, etc.</p> <p>Finally, if the full content was used (no <code>::: lead</code> found), it limits the result length.</p> A bookmarklet to create a new link content Markdown on GitHub 2023-02-08T09:42:51Z https://nicolas-hoizey.com/articles/2023/02/08/a-bookmarklet-to-create-a-new-link-content-markdown-on-github/ <div class="lead"> <p>When I was building my site on my local computer, I had a shell script to initialize a new Markdown file for sharing a <a href="https://nicolas-hoizey.com/links/">link</a>. When I <a href="https://nicolas-hoizey.com/notes/2022/07/29/1/">moved to Cloudflare Pages 6 months ago</a>, it opened a new opportunity to share links more easily in my Eleventy content, directly from the page I wanted to share. Bookmarklets are still an awesome invention!</p> </div> <p>The main features of my bookmarklet are:</p> <ul> <li>get the page title, ask for any change in a <code>window.prompt()</code></li> <li>get some content <ul> <li>the selection made on current page,</li> <li>or the page's meta description,</li> <li>or the first paragraph of the first <code>main</code> element,</li> <li>or the first paragraph of the first <code>article</code> element,</li> <li>or the first paragraph of the page</li> </ul> </li> <li>compute the slug based on the title</li> <li>compute the file path for my links content type (<code>/links/YYYY/MM/DD/slug/index.md</code>)</li> <li>create the Markdown file content, with YAML Front Matter</li> <li>open a new file editor on GitHub, so I can add some content and metadata</li> </ul> <p>I can then commit the file, push it directly to my <code>main</code> branch or open a pull request.</p> <p>And then the build runs on Cloudflare Pages, and the new link is online. It is also available in the feeds, to it soon becomes a toot on Mastodon, thanks to <a href="https://nicolas-hoizey.com/articles/2023/01/07/let-s-posse-to-mastodon-with-a-feed-and-a-github-action/">my GitHub Action</a>!</p> <p>The JavaScript source code <a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/main/assets/js/bookmarklets/new-link.js">is here on GitHub</a>:</p> <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// ==Bookmarklet==</span> <span class="token comment">// @name +🔗</span> <span class="token comment">// @description New link for nicolas-hoizey.com</span> <span class="token comment">// @version 1.0</span> <span class="token comment">// ==/Bookmarklet==</span> <span class="token comment">// Adapted from https://gist.github.com/codeguy/6684588#gistcomment-3361909</span> <span class="token keyword">const</span> <span class="token function-variable function">slugify</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">str</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">let</span> slug <span class="token operator">=</span> str<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">1: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">,</span> <span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">2: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">normalize</span><span class="token punctuation">(</span><span class="token string">'NFD'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">3: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">[\u0300-\u036f]</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">4: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">5: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\s+</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">6: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">[^\w ]+</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">7: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">8: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> slug <span class="token operator">=</span> slug<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex"> +</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">'-'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">9: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> slug<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token comment">/* ********************************************************************************** /* Get data from the page /* *********************************************************************************/</span> <span class="token keyword">let</span> pageTitle <span class="token operator">=</span> window<span class="token punctuation">.</span>document<span class="token punctuation">.</span>title<span class="token punctuation">;</span> <span class="token keyword">let</span> linkSelection <span class="token operator">=</span> <span class="token string">'getSelection'</span> <span class="token keyword">in</span> window <span class="token operator">?</span> window<span class="token punctuation">.</span><span class="token function">getSelection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">;</span> <span class="token keyword">let</span> linkContent <span class="token operator">=</span> linkSelection <span class="token operator">||</span> window<span class="token punctuation">.</span>document <span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'head meta[name=description i]'</span><span class="token punctuation">)</span> <span class="token operator">?.</span>content<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">||</span> window<span class="token punctuation">.</span>document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'main p'</span><span class="token punctuation">)</span><span class="token operator">?.</span>textContent<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">||</span> window<span class="token punctuation">.</span>document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'article p'</span><span class="token punctuation">)</span><span class="token operator">?.</span>textContent<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">||</span> window<span class="token punctuation">.</span>document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'p'</span><span class="token punctuation">)</span><span class="token operator">?.</span>textContent<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">let</span> linkUrl <span class="token operator">=</span> window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>href<span class="token punctuation">;</span> <span class="token comment">/* ********************************************************************************** /* Ask the user to confirm/modify the title /* *********************************************************************************/</span> <span class="token keyword">let</span> title <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">prompt</span><span class="token punctuation">(</span><span class="token string">'Title of the link?'</span><span class="token punctuation">,</span> pageTitle<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>title <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> slug <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">prompt</span><span class="token punctuation">(</span><span class="token string">'Slug of the link?'</span><span class="token punctuation">,</span> <span class="token function">slugify</span><span class="token punctuation">(</span>title<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>slug <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* ********************************************************************************** /* Build the content /* *********************************************************************************/</span> <span class="token keyword">const</span> today <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> dateString <span class="token operator">=</span> today <span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">'T'</span><span class="token punctuation">,</span> <span class="token string">' '</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\.[0-9]{3}Z</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span> <span class="token string">' +00:00'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">let</span> value <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">--- date: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dateString<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> title: "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" lang: en link: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>linkUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> authors: - "" tags: [] --- \n </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>linkContent <span class="token operator">?</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>linkContent<span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">,</span> <span class="token string">'\n> '</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span> <span class="token operator">:</span> <span class="token string">''</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> <span class="token comment">/* ********************************************************************************** /* Build the URL /* *********************************************************************************/</span> <span class="token keyword">const</span> pathDate <span class="token operator">=</span> dateString<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'-'</span><span class="token punctuation">,</span> <span class="token string">'/'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> filename <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">src/links/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>pathDate<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/index.md</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> <span class="token keyword">let</span> newFileUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://github.com/nhoizey/nicolas-hoizey.com/new/main/?filename=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;value=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">encodeURIComponent</span><span class="token punctuation">(</span> value <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> window<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span>newFileUrl<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>The metadata in the comment on top of this script is used by <a href="https://www.npmjs.com/package/bookmarklet">bookmarklet</a>, the npm package I use to transform my source JS into a proper bookmarklet, with <a href="https://nicolas-hoizey.com/tools/bookmarklets/new-link">a page from which I can drag the link to my bookmarks bar</a>:</p> <pre class="language-shell"><code class="language-shell">bookmarklet <span class="token parameter variable">--demo</span> assets/js/bookmarklets/new-link.js src/tools/bookmarklets/new-link.html</code></pre> <p>Good old bookmarklets are still great in 2023! đŸ„°</p> Updating webmentions on a static site 2023-02-05T19:27:38Z https://nicolas-hoizey.com/articles/2023/02/05/updating-webmentions-on-a-static-site/ <div class="lead"> <p>When <a href="https://nicolas-hoizey.com/articles/2017/07/27/so-long-disqus-hello-webmentions/">I started using Webmention on this site</a> (more than 5 years ago!), I was building the site on my local computer, and uploading the build result on my hosting with <code>rsync</code>. I've <a href="https://nicolas-hoizey.com/notes/2022/07/29/1/">moved to Cloudflare Pages 6 months ago</a>, which means webmentions where updated only when I pushed new content to GitHub. Here's how I fixed that.</p> </div> <p>I chose to fetch new webmentions directly on GitHub with an Action, so that new webmentions are immediately added to the repository, and future calls to the <a href="http://webmention.io/">webmention.io</a> API only ask for new mentions.</p> <div class="info"> <p>Most of my Webmention implementation is based on two great inspiration sources:</p> <ul> <li><a href="https://mxb.dev/">Max Böck</a>'s <a href="https://mxb.dev/blog/using-webmentions-on-static-sites/">Using Webmentions in Eleventy</a></li> <li><a href="https://sia.codes/">Sia</a>'s <a href="https://sia.codes/posts/webmentions-eleventy-in-depth/">An In-Depth Tutorial of Webmentions + Eleventy</a></li> </ul> </div> <p>Here's <a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/main/.github/workflows/update-webmentions.yml">the workflow of my GitHub Action</a>:</p> <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">name</span><span class="token punctuation">:</span> Check Webmentions <span class="token key atrule">on</span><span class="token punctuation">:</span> <span class="token key atrule">schedule</span><span class="token punctuation">:</span> <span class="token comment"># Runs at every 15th minute from 0 through 59</span> <span class="token comment"># https://crontab.guru/#0/15_*_*_*_*</span> <span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> <span class="token string">'0/15 * * * *'</span> <span class="token key atrule">workflow_dispatch</span><span class="token punctuation">:</span> <span class="token key atrule">concurrency</span><span class="token punctuation">:</span> <span class="token key atrule">group</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> github.workflow <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token key atrule">cancel-in-progress</span><span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">jobs</span><span class="token punctuation">:</span> <span class="token key atrule">webmentions</span><span class="token punctuation">:</span> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest <span class="token key atrule">steps</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Checkout the project <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4 <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Select Node.js version <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>node@v3 <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token key atrule">node-version-file</span><span class="token punctuation">:</span> <span class="token string">'.nvmrc'</span> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Install dependencies <span class="token key atrule">run</span><span class="token punctuation">:</span> npm ci <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Run webmention script <span class="token key atrule">env</span><span class="token punctuation">:</span> <span class="token key atrule">WEBMENTION_IO_TOKEN</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.WEBMENTION_IO_TOKEN <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> npm run webmention <span class="token punctuation">></span><span class="token punctuation">></span> $GITHUB_STEP_SUMMARY <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Create Pull Request <span class="token key atrule">uses</span><span class="token punctuation">:</span> peter<span class="token punctuation">-</span>evans/create<span class="token punctuation">-</span>pull<span class="token punctuation">-</span>request@v3 <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token key atrule">token</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.PAT <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token key atrule">branch</span><span class="token punctuation">:</span> webmentions <span class="token key atrule">delete-branch</span><span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">commit-message</span><span class="token punctuation">:</span> Update Webmentions <span class="token key atrule">title</span><span class="token punctuation">:</span> Update Webmentions <span class="token key atrule">labels</span><span class="token punctuation">:</span> automerge đŸ€ž </code></pre> <p>It uses the <code>WEBMENTION_IO_TOKEN</code> and <code>PAT</code> (<a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token">Personal Access Token</a>) secrets I've defined in my GitHub secrets for Actions.</p> <p>And here's <a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/main/_scripts/update-webmention.js">the Node.js script that it runs</a>:</p> <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> fetch <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'node-fetch'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> unionBy <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'lodash/unionBy'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> sanitizeHTML <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'sanitize-html'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> domain <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span><span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'../package.json'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>homepage<span class="token punctuation">)</span><span class="token punctuation">.</span>hostname<span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> writeToCache<span class="token punctuation">,</span> readFromCache <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'../src/_utils/cache'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Load .env variables with dotenv</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'dotenv'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Define Cache Location and API Endpoint</span> <span class="token keyword">const</span> <span class="token constant">WEBMENTION_URL</span> <span class="token operator">=</span> <span class="token string">'https://webmention.io/api'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token constant">WEBMENTION_CACHE</span> <span class="token operator">=</span> <span class="token string">'_cache/webmentions.json'</span><span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token constant">WEBMENTION_TOKEN</span> <span class="token operator">=</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">WEBMENTION_IO_TOKEN</span><span class="token punctuation">;</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">fetchWebmentions</span><span class="token punctuation">(</span><span class="token parameter">since<span class="token punctuation">,</span> perPage <span class="token operator">=</span> <span class="token number">10000</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// If we dont have a domain name or token, abort</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>domain <span class="token operator">||</span> <span class="token operator">!</span><span class="token constant">WEBMENTION_TOKEN</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">'>>> unable to fetch webmentions: missing domain or token'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">let</span> url <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">WEBMENTION_URL</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/mentions.jf2?domain=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>domain<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;token=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">WEBMENTION_TOKEN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;per-page=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>perPage<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>since<span class="token punctuation">)</span> url <span class="token operator">+=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&amp;since=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>since<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> <span class="token comment">// only fetch new mentions</span> <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">const</span> feed <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> webmentions <span class="token operator">=</span> feed<span class="token punctuation">.</span>children<span class="token punctuation">;</span> <span class="token keyword">let</span> cleanedWebmentions <span class="token operator">=</span> <span class="token function">cleanWebmentions</span><span class="token punctuation">(</span>webmentions<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cleanedWebmentions<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'[Webmention] No new webmention'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">[Webmention] </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>cleanedWebmentions<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> new webmentions</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> cleanedWebmentions<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">cleanWebmentions</span><span class="token punctuation">(</span><span class="token parameter">webmentions</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// https://mxb.dev/blog/using-webmentions-on-static-sites/#h-parsing-and-filtering</span> <span class="token keyword">const</span> <span class="token function-variable function">sanitize</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">entry</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// Sanitize HTML content</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> content <span class="token punctuation">}</span> <span class="token operator">=</span> entry<span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>content <span class="token operator">&amp;&amp;</span> content<span class="token punctuation">[</span><span class="token string">'content-type'</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'text/html'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> html <span class="token operator">=</span> content<span class="token punctuation">.</span>html<span class="token punctuation">;</span> html <span class="token operator">=</span> html <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">&lt;a [^>]+>&lt;\/a></span><span class="token regex-delimiter">/</span><span class="token regex-flags">gm</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\n</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">'&lt;br />'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> html <span class="token operator">=</span> <span class="token function">sanitizeHTML</span><span class="token punctuation">(</span>html<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">allowedTags</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">'b'</span><span class="token punctuation">,</span> <span class="token string">'i'</span><span class="token punctuation">,</span> <span class="token string">'em'</span><span class="token punctuation">,</span> <span class="token string">'strong'</span><span class="token punctuation">,</span> <span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token string">'blockquote'</span><span class="token punctuation">,</span> <span class="token string">'ul'</span><span class="token punctuation">,</span> <span class="token string">'ol'</span><span class="token punctuation">,</span> <span class="token string">'li'</span><span class="token punctuation">,</span> <span class="token string">'code'</span><span class="token punctuation">,</span> <span class="token string">'pre'</span><span class="token punctuation">,</span> <span class="token string">'br'</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token literal-property property">allowedAttributes</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">a</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'href'</span><span class="token punctuation">,</span> <span class="token string">'rel'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token literal-property property">img</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'src'</span><span class="token punctuation">,</span> <span class="token string">'alt'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token literal-property property">allowedIframeHostnames</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> content<span class="token punctuation">.</span>html <span class="token operator">=</span> html<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// Fix missing publication date</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>entry<span class="token punctuation">.</span>published <span class="token operator">&amp;&amp;</span> entry<span class="token punctuation">[</span><span class="token string">'wm-received'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> entry<span class="token punctuation">.</span>published <span class="token operator">=</span> entry<span class="token punctuation">[</span><span class="token string">'wm-received'</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> entry<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token keyword">return</span> webmentions<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>sanitize<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// Merge fresh webmentions with cached entries, unique per id</span> <span class="token keyword">function</span> <span class="token function">mergeWebmentions</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>b<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> a<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">let</span> union <span class="token operator">=</span> <span class="token function">unionBy</span><span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">,</span> <span class="token string">'wm-id'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> union<span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">let</span> aDate <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span>a<span class="token punctuation">.</span>published <span class="token operator">||</span> a<span class="token punctuation">[</span><span class="token string">'wm-received'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">let</span> bDate <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span>b<span class="token punctuation">.</span>published <span class="token operator">||</span> b<span class="token punctuation">[</span><span class="token string">'wm-received'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> aDate <span class="token operator">-</span> bDate<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> union<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">const</span> <span class="token function-variable function">updateWebmention</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> cached <span class="token operator">=</span> <span class="token function">readFromCache</span><span class="token punctuation">(</span><span class="token constant">WEBMENTION_CACHE</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token punctuation">{</span> <span class="token literal-property property">lastFetched</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token literal-property property">webmentions</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token comment">// Only fetch new mentions in production</span> <span class="token keyword">const</span> fetchedAt <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> newWebmentions <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchWebmentions</span><span class="token punctuation">(</span>cached<span class="token punctuation">.</span>lastFetched<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>newWebmentions<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> webmentions <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">lastFetched</span><span class="token operator">:</span> fetchedAt<span class="token punctuation">,</span> <span class="token literal-property property">webmentions</span><span class="token operator">:</span> <span class="token function">mergeWebmentions</span><span class="token punctuation">(</span>cached<span class="token punctuation">.</span>webmentions<span class="token punctuation">,</span> newWebmentions<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token function">writeToCache</span><span class="token punctuation">(</span>webmentions<span class="token punctuation">,</span> <span class="token constant">WEBMENTION_CACHE</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token function">updateWebmention</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Whenever the workflow updates the repository with new webmentions, it triggers a Cloudflare Pages build (could be Netlify), and the site is updated.</p> <p>It means I don't have to run a full build of the site periodically &quot;just&quot; to check if there are new webmentions, and the check can be more frequent, as it is really light and fast.</p> Let's POSSE to Mastodon with a Feed and a GitHub Action 2023-01-07T12:49:09Z https://nicolas-hoizey.com/articles/2023/01/07/let-s-posse-to-mastodon-with-a-feed-and-a-github-action/ <div class="lead"> <p>After building a Node script <a href="https://nicolas-hoizey.com/notes/2022/11/26/1/">for my own POSSE needs</a>, I thought it would be good if other people could also use it. I knew not many people would be able to use the script as-is, so I built a GitHub Action that is much simpler to use, without losing any feature, even gaining some!</p> </div> <p>You should already know that I'm a true believer of <a href="https://indieweb.org/">IndieWeb</a> and <a href="https://indieweb.org/POSSE">POSSE</a>, as <a href="https://nicolas-hoizey.com/archives/?query=indieweb">many contents I already published</a> show.</p> <p>You could for example replace &quot;Medium&quot; with many other services, including Twitter and Mastodon, in <a href="https://nicolas-hoizey.com/articles/2017/11/09/medium-is-only-an-edge-server-of-your-posse-cdn-your-own-blog-is-the-origin/">Medium is only an edge server of your POSSE CDN, your own blog is the origin</a>.</p> <p>I also gave a talk (in French) about IndieWeb and POSSE 3 years ago: <a href="https://nicolas-hoizey.com/talks/2019/10/10/ne-vous-laissez-plus-deposseder-de-vos-contenus/" hreflang="fr">Ne vous laissez plus dĂ©POSSEder de vos contenus !</a>.</p> <div class="info"> <p>Did you see Heydon Pickering's “Why The IndieWeb?” episode of the Webbed Briefs?<br /> <a href="https://briefs.video/videos/why-the-indieweb/">You should!</a></p> </div> <p>Every time I talk about IndieWeb and POSSE, a lot of people reply with “but it's not easy”
 and they are right!</p> <p>As <a href="https://nicolas-hoizey.com/links/2022/11/15/the-indieweb-for-everyone/">Max Böck recently said</a>:</p> <blockquote> <p>Owning your content on the web should not require extensive technical knowledge or special skills. It should be just as easy as signing up for a cellphone plan.</p> </blockquote> <p>So
</p> <p>I've developed <strong>a GitHub Action for anyone to POSSE their content to Mastodon as easily as possible</strong>: <a href="https://github.com/marketplace/actions/any-feed-to-mastodon">GitHub Action: Any feed to Mastoson</a>.</p> <p>It currently requires a JSON Feed for input, so you might still have to build this one, if you &quot;only&quot; have a RSS or Atom feed. I hope to <a href="https://github.com/nhoizey/github-action-jsonfeed-to-mastodon/issues/16">support these also in the future</a> as they're often available out of the box in content management tools/platforms (even on Mastodon), but there are multiple variants so it's not easy to deal with.</p> <p>I know there are already other ways to push content from RSS/Atom feeds to Mastodon, but I didn't want to rely on a third party service like IFTTT or Zapier. Ok, GitHub is also a 3rd party, but my code and content are already there anyway<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2023/01/07/let-s-posse-to-mastodon-with-a-feed-and-a-github-action/#fn1" id="fnref1">[1]</a></sup>. đŸ€·â€â™‚ïž</p> <p>I won't paraphrase the Action's documentation, so go read it, use it, and tell me if it's useful:<br /> <a href="https://github.com/marketplace/actions/any-feed-to-mastodon">GitHub Action: Any feed to Mastoson</a></p> <p>If you have some ideas, bugs or anything to discuss about this action, <a href="https://github.com/nhoizey/github-action-feed-to-mastodon/issues">GitHub issues</a> are the right place.</p> <p>Also, I know my code is not state of the art, so feel free to open issues, or even better pull requests, if you think you help improve it.</p> <p>HTH.</p> <hr class="footnotes-sep" /> <section class="footnotes"> <ol class="footnotes-list"> <li id="fn1" class="footnote-item"><p>and 99% of the Action is a Node script using the Mastodon API, so I can move anywhere else if necessary. <a href="https://nicolas-hoizey.com/articles/2023/01/07/let-s-posse-to-mastodon-with-a-feed-and-a-github-action/#fnref1" class="footnote-backref">↩</a></p> </li> </ol> </section> Cutting back on Instacrap 2022-05-18T16:23:01Z https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/ <div class="lead"> <p>I've been disliking for a long time that Instagram is a closed platform, more so in the hands of Facebook.</p> <p>I additionally now really dislike that they are <a href="https://www.newstatesman.com/science-tech/2021/07/instagram-pivot-video-tiktok-mosseri-reels-marks-end-social-media-we-know-it">doing everything to resemble TikTok</a>, by <a href="https://www.washingtonpost.com/technology/2021/07/02/instagram-tiktok-videos/">prioritizing video more and more, at the expense of photography</a>.</p> </div> <p>As a result, I'm going to stay on it for now, but only keep following people I really know and like. I will find inspiration and share my own creations in a much more enjoyable way on welcoming platforms like <a href="https://www.flickr.com/photos/nicolas-hoizey/">Flickr</a> and <a href="https://pixelfed.social/nhoizey">Pixelfed</a><sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/#fn1" id="fnref1">[1]</a></sup>.</p> <p>If you want to follow my photography work, I invite you to come directly to <strong>my own website</strong>: <a href="https://nicolas-hoizey.photo!/">nicolas-hoizey.photo</a>.</p> <p>Here are a few screenshot of the experience you can expect, tailored to my needs and your viewing pleasure:</p> <p><img src="https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/nicolas-hoizey-photo-site-01-home-page.png" alt="Screenshot of the home page" title="The home page" /></p> <p><img src="https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/nicolas-hoizey-photo-site-02-category.png" alt="Screenshot of a category page" title="A category page" /></p> <p><img src="https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/nicolas-hoizey-photo-site-03-photo-page.png" alt="Screenshot of a photo page" title="A photo page" /></p> <p><img src="https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/nicolas-hoizey-photo-site-04-map.png" alt="Screenshot of the page with a map of all photos" title="The map of all photos" /></p> <p><img src="https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/nicolas-hoizey-photo-site-05-blog-post.png" alt="Screenshot of the blog page" title="The blog" /></p> <p>Enjoy <a href="https://nicolas-hoizey.photo/">the visit</a>!</p> <hr class="footnotes-sep" /> <section class="footnotes"> <ol class="footnotes-list"> <li id="fn1" class="footnote-item"><p><a href="https://pixelfed.org/">Pixelfed</a> is kind of the (few) good parts of Instagram, with the many good part of Mastodon. Here's a <a href="https://write.wien.rocks/paula/beginners-guide-to-pixelfed">beginner's guide to Pixelfed</a>. <a href="https://nicolas-hoizey.com/articles/2022/05/18/cutting-back-on-instacrap/#fnref1" class="footnote-backref">↩</a></p> </li> </ol> </section> Accessible anchor links with Markdown-it and Eleventy 2021-02-25T21:08:34Z https://nicolas-hoizey.com/articles/2021/02/25/accessible-anchor-links-with-markdown-it-and-eleventy/ <div class="lead"> <p>I like to be able to link directly to a section in a long content. I wish every site provided anchor links associated to headings, even if <a href="https://web.dev/text-fragments/">Text Fragments</a> might be a cross browser thing sometimes in the future. Here's how I made the anchor links of my <a href="https://11ty.dev/">Eleventy</a> based site accessible.</p> </div> <p>I've been using the <a href="https://github.com/valeriangalliat/markdown-it-anchor">markdown-it-anchors</a> plugin in my Eleventy configuration for a while, but even if I applied some settings different from the defaults (which heading levels to consider, how to generate a slug, which visual symbol to use, etc.), I never tried to change the rendering function, as I thought the default one was enough.</p> <div class="heading-wrapper"> <h2 id="were-my-anchor-links-accessible" tabindex="-1">Were my Anchor Links Accessible?</h2> </div> <p>But a few weeks ago, I read this great detailed post where <a href="https://amberwilson.co.uk/">Amber Wilson</a> explains how she figured out how to make such anchor links really accessible: <a href="https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/">Are your Anchor Links Accessible?</a></p> <p>My anchor links were not accessible at all
 đŸ˜±</p> <div class="heading-wrapper"> <h2 id="enhancing-markdown-it-anchor-s-rendering" tabindex="-1">Enhancing <code>markdown-it-anchor</code>'s rendering</h2> </div> <p>Amber also uses Eleventy and shared <a href="https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/#automating-accessible-anchor-links">a new plugin to automate such accessible anchor links</a>, but I wanted to keep the features I'm already using in <code>markdown-it-anchor</code> and enhance it with better accessibility.</p> <p>Fortunately, <code>markdown-it-anchor</code> provides <a href="https://github.com/valeriangalliat/markdown-it-anchor#usage">a large set of options</a>, including a way to provide our own rendering function with the <code>renderPermalink</code> option. After a while diving into <code>markdown-it</code> and <code>markdown-it-anchor</code> documentation and code, I've been able to create a rendering function that generates accessible anchor links, which you should be able to use in any Eleventy project! 🎉</p> <p>The code is primarily based on <a href="https://github.com/valeriangalliat/markdown-it-anchor/blob/85afd1f054032d6a3c83102329c413b56cad99a9/index.js#L13-L34"><code>markdown-it-anchor</code>'s default <code>renderPermalink</code> function</a>.</p> <p>Here is <a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/4c9e42b306a387e9533a1036a6286b7f24091ed4/.eleventy.js#L111-L176">my version</a>:</p> <pre class="language-javascript"><code class="language-javascript"><span class="token function-variable function">renderPermalink</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">slug<span class="token punctuation">,</span> opts<span class="token punctuation">,</span> state<span class="token punctuation">,</span> idx</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// based on fifth version in</span> <span class="token comment">// https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/</span> <span class="token keyword">const</span> linkContent <span class="token operator">=</span> state<span class="token punctuation">.</span>tokens<span class="token punctuation">[</span>idx <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>children<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>content<span class="token punctuation">;</span> <span class="token comment">// Create the openning &lt;div> for the wrapper</span> <span class="token keyword">const</span> headingWrapperTokenOpen <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span> <span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'div_open'</span><span class="token punctuation">,</span> <span class="token string">'div'</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">attrs</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token string">'class'</span><span class="token punctuation">,</span> <span class="token string">'heading-wrapper'</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Create the closing &lt;/div> for the wrapper</span> <span class="token keyword">const</span> headingWrapperTokenClose <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span> <span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'div_close'</span><span class="token punctuation">,</span> <span class="token string">'div'</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">attrs</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token string">'class'</span><span class="token punctuation">,</span> <span class="token string">'heading-wrapper'</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Create the tokens for the full accessible anchor link</span> <span class="token comment">// &lt;a class="deeplink" href="#your-own-platform-is-the-nearest-you-can-get-help-to-setup"></span> <span class="token comment">// &lt;span aria-hidden="true"></span> <span class="token comment">// ${opts.permalinkSymbol}</span> <span class="token comment">// &lt;/span></span> <span class="token comment">// &lt;span class="visually-hidden"></span> <span class="token comment">// Section titled Your "own" platform is the nearest you can(get help to) setup</span> <span class="token comment">// &lt;/span></span> <span class="token comment">// &lt;/a ></span> <span class="token keyword">const</span> anchorTokens <span class="token operator">=</span> <span class="token punctuation">[</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'link_open'</span><span class="token punctuation">,</span> <span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">attrs</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token operator">...</span><span class="token punctuation">(</span>opts<span class="token punctuation">.</span>permalinkClass <span class="token operator">?</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token string">'class'</span><span class="token punctuation">,</span> opts<span class="token punctuation">.</span>permalinkClass<span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">'href'</span><span class="token punctuation">,</span> opts<span class="token punctuation">.</span><span class="token function">permalinkHref</span><span class="token punctuation">(</span>slug<span class="token punctuation">,</span> state<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token operator">...</span>Object<span class="token punctuation">.</span><span class="token function">entries</span><span class="token punctuation">(</span>opts<span class="token punctuation">.</span><span class="token function">permalinkAttrs</span><span class="token punctuation">(</span>slug<span class="token punctuation">,</span> state<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'span_open'</span><span class="token punctuation">,</span> <span class="token string">'span'</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">attrs</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token string">'aria-hidden'</span><span class="token punctuation">,</span> <span class="token string">'true'</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'html_block'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">content</span><span class="token operator">:</span> opts<span class="token punctuation">.</span>permalinkSymbol<span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'span_close'</span><span class="token punctuation">,</span> <span class="token string">'span'</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'span_open'</span><span class="token punctuation">,</span> <span class="token string">'span'</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">attrs</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token string">'class'</span><span class="token punctuation">,</span> <span class="token string">'visually-hidden'</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'html_block'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Section titled </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>linkContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'span_close'</span><span class="token punctuation">,</span> <span class="token string">'span'</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">state<span class="token punctuation">.</span>Token</span><span class="token punctuation">(</span><span class="token string">'link_close'</span><span class="token punctuation">,</span> <span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// idx is the index of the heading's first token</span> <span class="token comment">// insert the wrapper opening before the heading</span> state<span class="token punctuation">.</span>tokens<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span>idx<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> headingWrapperTokenOpen<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// insert the anchor link tokens after the wrapper opening and the 3 tokens of the heading</span> state<span class="token punctuation">.</span>tokens<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span>idx <span class="token operator">+</span> <span class="token number">3</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token operator">...</span>anchorTokens<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// insert the wrapper closing after all these</span> state<span class="token punctuation">.</span>tokens<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span> idx <span class="token operator">+</span> <span class="token number">3</span> <span class="token operator">+</span> <span class="token number">1</span> <span class="token operator">+</span> anchorTokens<span class="token punctuation">.</span>length<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> headingWrapperTokenClose <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre> <p>I hope there are enough comments in the code to understand how it works. The main <code>markdown-it</code> behavior I had to understand is that <a href="https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#token-stream">it uses an array of tokens to represent HTML nodes</a>, instead of a more traditional <a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree">Abstract Syntax Tree</a>.</p> <div class="heading-wrapper"> <h2 id="adapting-the-css-to-the-new-html-structure" tabindex="-1">Adapting the CSS to the new HTML structure</h2> </div> <p>If you're already using <code>markdown-it-anchor</code>, the anchor link is inside the heading:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h3</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>were-my-anchor-links-accessible<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> Were my Anchor Links Accessible? <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>deeplink<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#were-my-anchor-links-accessible<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>#<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h3</span><span class="token punctuation">></span></span></code></pre> <p>With my new code, following Amber advice, it is now:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>heading-wrapper<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>were-my-anchor-links-accessible<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Were my Anchor Links Accessible?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>deeplink<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#were-my-anchor-links-accessible<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">aria-hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>#<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>visually-hidden<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Section titled Were my Anchor Links Accessible?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre> <p>My actual code is a little more complex as I use a SVG for the anchor symbol, but the HTML structure is the same, so you can take some inspiration from my CSS code, which is heavily inspired from Stephanie Eckles' <a href="https://smolcss.dev/#smol-article-anchors">Smol Article Anchors</a>:</p> <pre class="language-css"><code class="language-css"><span class="token selector">// Anchor links // Based on https://smolcss.dev/#smol-article-anchors .heading-wrapper</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> // anchor link on the far right for long wrapping headings <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">minmax</span><span class="token punctuation">(</span>auto<span class="token punctuation">,</span> max-content<span class="token punctuation">)</span> min-content<span class="token punctuation">;</span> <span class="token property">align-items</span><span class="token punctuation">:</span> stretch<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.deeplink</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">justify-content</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span> <span class="token property">align-content</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span> <span class="token selector">&amp;:link, &amp;:visited</span> <span class="token punctuation">{</span> <span class="token property">padding</span><span class="token punctuation">:</span> 0 0.25rem<span class="token punctuation">;</span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 0.3em<span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-meta<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">text-decoration</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token selector">svg</span> <span class="token punctuation">{</span> <span class="token property">fill</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token property">stroke</span><span class="token punctuation">:</span> currentColor<span class="token punctuation">;</span> <span class="token property">stroke-width</span><span class="token punctuation">:</span> 2px<span class="token punctuation">;</span> <span class="token property">stroke-linecap</span><span class="token punctuation">:</span> round<span class="token punctuation">;</span> <span class="token property">stroke-linejoin</span><span class="token punctuation">:</span> round<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token selector">.heading-wrapper:hover &amp;, &amp;:hover, &amp;:focus</span> <span class="token punctuation">{</span> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-link-hover<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">background-color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-link-hover-bg<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 65rem<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.heading-wrapper</span> <span class="token punctuation">{</span> // Anchor link in the left margin on larger viewports <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> min-content auto<span class="token punctuation">;</span> <span class="token property">margin-left</span><span class="token punctuation">:</span> -2rem<span class="token punctuation">;</span> // 1rem width + .25rem * 2 paddings + 0.5rem gap <span class="token punctuation">}</span> <span class="token selector">.deeplink</span> <span class="token punctuation">{</span> <span class="token property">grid-row-start</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>On viewports <code>&lt; 65rem</code>, the anchor link is inside the content container, at the right of the heading. If a long heading wraps on multiple lines, the anchor link is located on the far right, but if the heading is short, the anchor link follows it directly. I'm not sure setting <code>grid-template-columns: minmax(auto, max-content) min-content;</code> is the best way to do it, feel free to suggest an enhancement.</p> <p>On viewports <code>&gt;= 65rem</code>, there is space around the content, so I move the anchor link in the margin on the left.</p> <div class="heading-wrapper"> <h2 id="enhancing-markdown-it-anchor-for-everyone" tabindex="-1">Enhancing <code>markdown-it-anchor</code> for everyone</h2> </div> <p>I asked <a href="https://www.codejam.info/val.html">ValĂ©rian Galliat</a>, maintainer of <code>markdown-it-anchor</code>, if he would be open to merge a pull request providing this enhancement: <a href="https://github.com/valeriangalliat/markdown-it-anchor/issues/82">https://github.com/valeriangalliat/markdown-it-anchor/issues/82</a></p> <p>But I think this would break (at least visually) all current uses of the plugin, so I believe it would require a new option to activate it. We'll discuss this before I provide the PR.</p> Enhancing archives navigation, step 2 2020-11-01T22:55:51Z https://nicolas-hoizey.com/articles/2020/11/02/enhancing-archives-navigation-step-2/ <div class="lead"> <p>In my previous article <a href="https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/">Enhancing archives navigation, step 1</a>, I promised further archives navigation enhancements. Here they are!</p> </div> <p>Remember how UX of navigation in archives by year and month where already enhanced with <a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/master/src/_layouts/archives.njk">a single Eleventy layout</a>:</p> <p><img src="https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/months-pagination-after.jpg" alt="User friendly months navigation with Eleventy only" /></p> <div class="warning"> <p>This new awesome layout made my build time go from 40 seconds to 300 seconds, a 650 % increase, not so awesome
 😅</p> </div> <p>Now, imagine you want to see content from two — or more — types (<a href="https://nicolas-hoizey.com/archives/?type=articles&amp;type=notes">articles and notes</a> for example), or mix not only one type and a date, but also the language, or tags, even multiple of them.</p> <p>Generating all possible filter combination as static pages with one single Eleventy build would probably take more than one hour. I obviously don't want that, even if this would provide users with an even better UX.</p> <p>Time to <a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/">enhance the already nice server-side rendering with awesome client-side features</a>!</p> <p>Here's what is now available for navigating the archives, <a href="https://kryogenix.org/code/browser/everyonehasjs.html">if you activated JavaScript</a> in your browser:</p> <p><img src="https://nicolas-hoizey.com/articles/2020/11/02/enhancing-archives-navigation-step-2/archives-live-search-with-algolia.jpg" alt="Navigating the archives with search and facets" /></p> <p>There is a search input field, to search for any content, with live &quot;as you type&quot; results, and live updated filtering facets. đŸ€Ż</p> <p>If you search for something specific, the results highlight why they're here in the list:</p> <p><img src="https://nicolas-hoizey.com/articles/2020/11/02/enhancing-archives-navigation-step-2/archives-live-search-with-algolia-highlight.jpg" alt="Highlighted results" /></p> <p><img src="https://nicolas-hoizey.com/assets/logos/algolia.png" alt="Algolia logo" class="logo" /><br /> All of this would not be possible without <a href="https://nicolas-hoizey.com/tags/algolia/">Algolia</a>, the awesome search service I've been using for multiple years.</p> <p>I inject all my contents in an Algolia index, and a single JavaScript script uses <a href="https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/">Algolia's InstantSearch.js UI library</a> to build the user interface and synchronize the search term and facets values to the URL (and back).</p> <p>What I really like here is that this is not the only way to browse the archives, it is &quot;only&quot; a (great) enhancement of what's available to anyone with the server-side rendering.</p> <p>I hope you'll enjoy this new feature!</p> Enhancing archives navigation, step 1 2020-10-26T14:16:23Z https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/ <div class="lead"> <p>I decided years ago to remove paged navigation (aka &quot;pagination&quot;), because I find it not user friendly at all, and a nightmare for SEO with new content pushing one tenth of contents to another page (for a 10 items per page pagination). Now, I improved the UX even further.</p> </div> <p>Here's how the month by month navigation was presented earlier, in the page bottom:</p> <p><img src="https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/months-pagination-before.jpg" alt="The ugly and not user friendly months navigation before" /></p> <p>While it was OK for the year by year navigation, such a full months list was really not user friendly, so I intended to enhance it a little.</p> <p>Instead of &quot;just&quot; a little, I finaly chose to present this navigation with facets in a search engine, with the possibility to combine a filter for the content type, and another for the year or month of publication:</p> <p><img src="https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/months-pagination-after.jpg" alt="A much more user friendly months navigation" /></p> <p>For example, you can navigate to the links I published in March 2019: <a href="https://nicolas-hoizey.com/links/2019/03/">/links/2019/03/</a>.</p> <p>I find it so easy to navigate, I wonder why I didn't have this idea earlier!</p> <p>I already had the required <a href="https://github.com/nhoizey/nicolas-hoizey.com/tree/master/src/_11ty/collections">Eleventy collections</a>, so almost everything was done in a single shared Nunjucks template. Not the easiest one, I recon.</p> <p>Now, I wonder if it's useful to keep the main navigation items for &quot;<a href="https://nicolas-hoizey.com/articles/">articles</a>&quot;, &quot;<a href="https://nicolas-hoizey.com/links/">links</a>&quot;, etc., or if the &quot;<a href="https://nicolas-hoizey.com/archives/">archives</a>&quot; navigation item is enough, which would obviously help on mobile.</p> <p>As you might have guessed from this article's title, I'm working on further archives navigation enhancements, we'll see that soon online, and I'll explain in a dedicated article. Stay tuned!</p> How I build my SVG sprites 2020-10-14T22:02:19Z https://nicolas-hoizey.com/articles/2020/10/15/how-i-build-my-svg-sprites/ <div class="lead"> <p>I'm using an SVG sprite on this site to make sure I don't repeat SVG code for icons that are used multiple times, and I inline it so the rendering doesn't depend on another resource loading. Here's how I build this sprite from individual SVG icons.</p> </div> <p><img src="https://nicolas-hoizey.com/assets/logos/feather-icons.png" alt="Feather icons" class="logo" /><br /> I'm using the very nice and open source <a href="https://feathericons.com/">Feather icons</a>. Feather provides <a href="https://github.com/feathericons/feather#usage">multiples ways to use the icons</a>, including a sprite, but it contains all icons and weights almost 60 KB (minified, not compressed), so it's really not a good option for me as I need only 9 of them. Feather also provides a way to load icons with JavaScript, obviously not the best choice either for performance in a statically generated site.</p> <p>It's nice anyway that there's <a href="https://www.npmjs.com/package/feather-icons"><code>feather-icons</code> on npm</a>, as adding it to my dev dependencies is enough to make sure I'll will always have the latest versions.</p> <p>Here's own these icons are used on this site:</p> <p><img src="https://nicolas-hoizey.com/articles/2020/10/15/how-i-build-my-svg-sprites/feather-icons-in-metas.png" alt="Feather icons in content meta datas" class="border" /></p> <p>In pages listing multiple contents such as the home page, these icons are used multiple times, so it's best having each only once in the HTML, hence the use of a sprite. For the whole site, I use only 9 different icons, so the sprite is light, at only 3 KB (minified, not compressed).</p> <p>To build this sprite, I use <a href="https://www.npmjs.com/package/svgstore"><code>svgstore</code></a> in the following Node.js script, where comments should be enough to understand how it works:</p> <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> svgstore <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'svgstore'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> path <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'path'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Where are Feather icons available from the npm package?</span> <span class="token keyword">const</span> <span class="token constant">ICONS_FOLDER</span> <span class="token operator">=</span> <span class="token string">'node_modules/feather-icons/dist/icons/'</span><span class="token punctuation">;</span> <span class="token comment">// Which icons do I need for the sprite?</span> <span class="token comment">// icon filename + title for accessibility</span> <span class="token keyword">const</span> <span class="token constant">ICONS_LIST</span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">calendar</span><span class="token operator">:</span> <span class="token string">'Date'</span><span class="token punctuation">,</span> <span class="token literal-property property">info</span><span class="token operator">:</span> <span class="token string">'Info'</span><span class="token punctuation">,</span> <span class="token literal-property property">link</span><span class="token operator">:</span> <span class="token string">'Link'</span><span class="token punctuation">,</span> <span class="token literal-property property">wifi</span><span class="token operator">:</span> <span class="token string">'Online'</span><span class="token punctuation">,</span> <span class="token string-property property">'wifi-off'</span><span class="token operator">:</span> <span class="token string">'Offline'</span><span class="token punctuation">,</span> <span class="token literal-property property">search</span><span class="token operator">:</span> <span class="token string">'Search'</span><span class="token punctuation">,</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'Tag'</span><span class="token punctuation">,</span> <span class="token literal-property property">twitter</span><span class="token operator">:</span> <span class="token string">'Twitter'</span><span class="token punctuation">,</span> <span class="token string-property property">'message-circle'</span><span class="token operator">:</span> <span class="token string">'Webmention'</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token comment">// Initiate the sprite with svgstore</span> <span class="token keyword">let</span> sprite <span class="token operator">=</span> <span class="token function">svgstore</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token comment">// Add these attributes to the sprite SVG</span> <span class="token literal-property property">svgAttrs</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">style</span><span class="token operator">:</span> <span class="token string">'display: none;'</span><span class="token punctuation">,</span> <span class="token string-property property">'aria-hidden'</span><span class="token operator">:</span> <span class="token string">'true'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// Copy these attributes from the icon source SVG to the symbol in the sprite</span> <span class="token literal-property property">copyAttrs</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'width'</span><span class="token punctuation">,</span> <span class="token string">'height'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Loop through each icon in the list</span> Object<span class="token punctuation">.</span><span class="token function">entries</span><span class="token punctuation">(</span><span class="token constant">ICONS_LIST</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">[</span>icon<span class="token punctuation">,</span> title<span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// Log the name of the icon and its title to the console</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>icon<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.svg -> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> svgFile <span class="token operator">=</span> fs <span class="token comment">// Load the content of the icon SVG file</span> <span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span>path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">ICONS_FOLDER</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>icon<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.svg</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">'utf8'</span><span class="token punctuation">)</span> <span class="token comment">// Make its dimensions relative to the surounding text</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">' width="24" height="24"'</span><span class="token punctuation">,</span> <span class="token string">' width="1em" height="1em"'</span><span class="token punctuation">)</span> <span class="token comment">// Remove useless attributes (for my usage) and add a title for accessibility</span> <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex"> fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-[^"]+"></span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> >&lt;title id="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>icon<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-icon"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/title></span><span class="token template-punctuation string">`</span></span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Add the new symbol to the sprite</span> sprite<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">symbol-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>icon<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> svgFile<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token comment">// Add attributes for accessibility</span> <span class="token literal-property property">symbolAttrs</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string-property property">'aria-labelledby'</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>icon<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-icon</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token literal-property property">role</span><span class="token operator">:</span> <span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Finally, store the sprite in a file Eleventy will be able to include</span> fs<span class="token punctuation">.</span><span class="token function">writeFileSync</span><span class="token punctuation">(</span> <span class="token string">'src/_includes/svg-sprite.svg'</span><span class="token punctuation">,</span> sprite<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">inline</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>This script is located in <a href="https://github.com/nhoizey/nicolas-hoizey.com/tree/master/src/_utils">the <code>src/_utils/</code> folder</a> of my Eleventy project, with other scripts I run with <code>npm</code>, like <a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/#server-side-first">my Webmention update script</a>.</p> <p>This script was previously part of my site build, but it made no sense, as the list of icons doesn't change much, and each icons are not often updated in the source. I just have to run <code>npm run svg</code> when I feel it useful, and my build is a little faster.</p> Identify which Apache rewrite rules are used 2020-05-29T10:13:01Z https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/ <div class="lead"> <p>I have many rewrite rules in my Apache configuration for redirections, some dating from more than 15 years ago. So I wanted to know which ones are really useful, because there's maybe some cleaning to do. I'll explain here how I got the list.</p> </div> <div class="heading-wrapper"> <h2 id="get-some-logs" tabindex="-1">Get some logs</h2> </div> <p>First, I had to tell <a href="https://httpd.apache.org/docs/2.4/en/mod/mod_rewrite.html">Apache's <code>mod_rewrite</code> module</a><sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn1" id="fnref1">[1]</a></sup> to log more information than it usually does, but not too much either.</p> <p>Here's what I added to my Apache configuration<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn2" id="fnref2">[2]</a></sup> with the <a href="https://httpd.apache.org/docs/2.4/en/mod/core.html#loglevel">LogLevel directive</a>:</p> <pre class="language-apacheconf"><code class="language-apacheconf"><span class="token directive-inline property">LogLevel</span> warn rewrite:trace2</code></pre> <p><code>warn</code> is Apache's default log level, and <code>trace2</code> is much more verbose, so I add it only for the <code>rewrite</code> module.</p> <div class="heading-wrapper"> <h2 id="filter-the-logs-for-useful-informations" tabindex="-1">Filter the logs for useful informations</h2> </div> <p>The logs I get with this are really verbose, and contain messages from Apache and all the active modules.</p> <p>Here is just a small extract for <strong>one single request</strong> to <code>articles/2018/06/users-do-change-font-size/</code>.</p> <p>This is an old URL format where I didn't put the day as I do now, so there's a redirection:</p> <pre><code>[Thu May 28 23:28:29.299495 2020] [rewrite:trace2] [pid 1241533:tid 140624745461504] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe7104af0a0/initial] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] rewrite 'articles/2018/06/users-do-change-font-size/' -&gt; 'https://nicolas-hoizey.com/articles/2018/06/15/users-do-change-font-size/' [Thu May 28 23:28:29.299518 2020] [rewrite:trace2] [pid 1241533:tid 140624745461504] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe7104af0a0/initial] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] explicitly forcing redirect with https://nicolas-hoizey.com/articles/2018/06/15/users-do-change-font-size/ [Thu May 28 23:28:29.299528 2020] [rewrite:trace2] [pid 1241533:tid 140624745461504] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe7104af0a0/initial] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] trying to replace prefix /home/nhoizey/www/nicolas-hoizey.com/www/ with / [Thu May 28 23:28:29.299531 2020] [rewrite:trace1] [pid 1241533:tid 140624745461504] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe7104af0a0/initial] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] escaping https://nicolas-hoizey.com/articles/2018/06/15/users-do-change-font-size/ for redirect [Thu May 28 23:28:29.299534 2020] [rewrite:trace1] [pid 1241533:tid 140624745461504] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe7104af0a0/initial] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] redirect to https://nicolas-hoizey.com/articles/2018/06/15/users-do-change-font-size/ [REDIRECT/301] [Thu May 28 23:28:29.327806 2020] [rewrite:trace1] [pid 1241533:tid 140624871286528] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe7100390a0/initial] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] pass through /home/nhoizey/www/nicolas-hoizey.com/www/articles/2018/06/15/users-do-change-font-size/ [Thu May 28 23:28:29.327852 2020] [rewrite:trace1] [pid 1241533:tid 140624871286528] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe6e47970a0/subreq] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] pass through /home/nhoizey/www/nicolas-hoizey.com/www/articles/2018/06/15/users-do-change-font-size/index.php [Thu May 28 23:28:29.327872 2020] [rewrite:trace1] [pid 1241533:tid 140624871286528] mod_rewrite.c(483): [client 92.169.204.166:0] 92.169.204.166 - - [nicolas-hoizey.com/sid#7fe7132989d8][rid#7fe6e479d0a0/subreq] [perdir /home/nhoizey/www/nicolas-hoizey.com/www/] pass through /home/nhoizey/www/nicolas-hoizey.com/www/articles/2018/06/15/users-do-change-font-size/index.html </code></pre> <p>The log file contains many lines like this, for all requests, on multiple domains sharing the same host. I had <strong>more than 4000 log lines per day</strong>, even before adding the detailed logs for the <code>rewrite</code> module, so it's impossible to navigate in these to get the information.</p> <p>Only the first of the eight lines above is useful to identify the redirection.</p> <p>So I chose to use simple shell tools<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn3" id="fnref3">[3]</a></sup> to filter the raw data and get only the useful lines.</p> <pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> apache.log <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">'nicolas-hoizey.com'</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"] rewrite '"</span></code></pre> <p><code>cat</code> prints the content of the log file on the standard output, and then <code>grep</code> filters lines containing three strings:</p> <ul> <li><code>nicolas-hoizey.com</code> to get only logs for my domain<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn4" id="fnref4">[4]</a></sup></li> <li><code>] rewrite '</code> to make sure I get the rewrite instruction, not the others (<code>explicitly forcing redirect</code>, <code>trying to replace prefix</code>, <code>escaping</code>, etc.)</li> </ul> <p>So with that, I get all lines for redirections, great first step.</p> <p>But the useful data is &quot;hidden&quot; at the end of the line, after many things that can probably be useful sometimes, but not for my current use case.</p> <div class="heading-wrapper"> <h2 id="extract-the-useful-value-from-the-remaining-logs" tabindex="-1">Extract the useful value from the remaining logs</h2> </div> <p>Here is the full line broken into pieces (I won't explain it, just split the parts):</p> <ul> <li><code>[Thu May 28 23:28:29.299495 2020]</code></li> <li><code>[rewrite:trace2]</code></li> <li><code>[pid 1241533:tid 140624745461504]</code></li> <li><code>mod_rewrite.c(483):</code></li> <li><code>[client 92.169.204.166:0]</code></li> <li><code>92.169.204.166</code></li> <li><code>- -</code></li> <li><code>[nicolas-hoizey.com/sid#7fe7132989d8]</code></li> <li><code>[rid#7fe7104af0a0/initial]</code></li> <li><code>[perdir /home/nhoizey/www/nicolas-hoizey.com/www/]</code></li> <li><code>rewrite 'articles/2018/06/users-do-change-font-size/' -&gt; 'https://nicolas-hoizey.com/articles/2018/06/15/users-do-change-font-size/'</code></li> </ul> <p>The only useful part is the last one. I can remove everything before.</p> <p>I chose to use <code>sed</code><sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn5" id="fnref5">[5]</a></sup> to replace everything from the beginning of the line to the <code>] rewrite </code> string:</p> <pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> apache.log <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">'nicolas-hoizey.com'</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"] rewrite '"</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/^.*\] rewrite //'</span></code></pre> <p>I then chose to get the list as a CSV file, so I also replaced the arrow in the middle (<code>-&gt;</code>) with a semi-colon, and I removed the domain from the second part to ease reading it:</p> <pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> apache.log <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">'nicolas-hoizey.com'</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"] rewrite '"</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/^.*\] rewrite //'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/ -> /;/'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/https:\/\/nicolas-hoizey.com\///'</span></code></pre> <p>Here's what I now get:</p> <pre><code>'articles/2018/06/users-do-change-font-size/';'articles/2018/06/15/users-do-change-font-size/' </code></pre> <p>But with logs for multiple days, I get some identical redirections many times, so I chose to <a href="https://unix.stackexchange.com/a/263849">sort them, keep only one of each, and count their number</a>:</p> <pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> apache.log <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">'nicolas-hoizey.com'</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"] rewrite '"</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/^.*\] rewrite //'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/ -> /;/'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/https:\/\/nicolas-hoizey.com\///'</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-nr</span></code></pre> <p>Finally, I chose to put all this in a file for later use:</p> <pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> apache.log <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">'nicolas-hoizey.com'</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"] rewrite '"</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/^.*\] rewrite //'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/ -> /;/'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/https:\/\/nicolas-hoizey.com\///'</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-nr</span> <span class="token operator">></span> ~/rewrites.csv</code></pre> <p>I can now run this script, open the <code>.csv</code> file in a spreadsheet, and see which of my redirections are still useful.</p> <p>After less than one single day with the log directive, I already have 181 different redirections performed. I will wait for a few days (24 hours later, I have <strong>839 different redirections</strong>), and I'll have to understand which ones are legitimate, and which others I can safely remove.</p> <p>For some of these redirections, the log also contains the referer, so I might be able to fix the URL at the source, like <a href="https://indieweb.org/wiki/index.php?title=Webmention&amp;type=revision&amp;diff=70110&amp;oldid=69713">I just did in the IndieWeb wiki</a>.</p> <hr class="footnotes-sep" /> <section class="footnotes"> <ol class="footnotes-list"> <li id="fn1" class="footnote-item"><p>I use <code>mod_rewrite</code> for redirections because I need advanced URL manipulations <a href="https://httpd.apache.org/docs/2.4/en/mod/mod_alias.html"><code>mod_alias</code></a> doesn't allow. <a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref1" class="footnote-backref">↩</a></p> </li> <li id="fn2" class="footnote-item"><p>Unfortunately, this can not be set in an <a href="https://httpd.apache.org/docs/2.4/en/howto/htaccess.html"><code>.htaccess</code> file</a> in your <code>DOCUMENT_ROOT</code>, so you have to be able to change your Apache configuration, like my hosting <a href="https://www.alwaysdata.com/en/">AlwaysData</a> allows. <a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref2" class="footnote-backref">↩</a></p> </li> <li id="fn3" class="footnote-item"><p>I'm sure there are better tools, more powerful, but these ones allowed me to get the result I wanted. <a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref3" class="footnote-backref">↩</a></p> </li> <li id="fn4" class="footnote-item"><p>you might have a log file dedicated to one single domain, which would be easier to parse. <a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref4" class="footnote-backref">↩</a></p> </li> <li id="fn5" class="footnote-item"><p>I know <code>awk</code> is more powerful, but I always forget the syntax, so <code>sed</code> is fine. KISS. <a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref5" class="footnote-backref">↩</a></p> </li> </ol> </section> JAMstack is fast only if you make it so 2020-05-05T10:13:01Z https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/ <div class="lead"> <p>JAMstack often promotes itself as an excellent way to provide performant sites. It's even the first listed benefit on <a href="https://jamstack.wtf/">jamstack.wtf</a>, a &quot;guide [which] gathers the concept of JAMstack in a straight-forward guide to encourage other developers to adopt the workflow&quot;. But too many JAMstack sites are very slow.</p> </div> <div lang="fr" class="info"> <p>Vous pouvez aussi lire la <a href="https://jamstatic.fr/2020/10/05/la-jamstack-n-est-rapide-que-si-vous-la-rendez-rapide/">version française</a>, merci <a href="https://arnaudligny.fr/">Arnaud</a> pour la traduction.</p> </div> <p>You may have seen <a href="https://infrequently.org/">Alex Russell</a>'s frequent rants about Gatsby:</p> <p><a href="https://twitter.com/slightlylate/status/1184959830819106816">https://twitter.com/slightlylate/status/1184959830819106816</a></p> <p>Gatsby is an easy target (among many others) because it is currently not optimized for performance out of the box, despite what's <a href="https://store.gatsbyjs.org/product/gatsby-sticker-6-pack">promoted</a>. It is possible to fix it, for example with <a href="https://www.gatsbyjs.org/packages/gatsby-plugin-no-javascript/">this plugin</a>, and I believe good React developers can make it shine, but it should be the default, not an afterthought.</p> <p>Eleventy is very different, as Zach Leatherman reminds us in <a href="https://www.zachleat.com/web/performance-dashboard/">Eleventy’s New Performance Leaderboard</a>:</p> <blockquote> <p>Eleventy doesn’t do any special optimizations out of the box to make your sites fast. It doesn’t protect you from making a slow site. But importantly <strong>it also doesn’t add anything extra either</strong>.</p> </blockquote> <p>The issue with most slow JAMstack sites is that they load a loooot of JavaScript. Remember that any added JavaScript has to be sent to the browser, which also needs more computation for it. <a href="https://v8.dev/blog/cost-of-javascript-2019">It quickly impacts performance</a>.</p> <p>Sometimes, using the server-side build is enough to get data from an API and serve HTML to all visitors, which is much better for performance.</p> <p>For example, <a href="https://www.swyx.io/">swyx</a> wrote <a href="https://www.swyx.io/writing/clientside-webmentions/">Clientside Webmentions</a> about implementing Webmention with <a href="https://svelte.dev/">Svelte</a>. Any article promoting <a href="https://nicolas-hoizey.com/tags/webmention/">Webmention</a> and easing its adoption is welcome! But even if it's nice for a demo of Webmention and Svelte, I wouldn't recommend doing it client-side.</p> <div class="heading-wrapper"> <h2 id="server-side-first" tabindex="-1">Server-side first</h2> </div> <p>I prefer <a href="https://nicolas-hoizey.com/articles/2017/07/27/so-long-disqus-hello-webmentions/#how-does-it-work-on-this-site">doing it on the server</a>.</p> <p>It allows to:</p> <ul> <li>call <a href="http://webmention.io/">webmention.io</a> API only when building the site, which should be less often than visitors viewing pages.</li> <li>cache the result of requests to <a href="https://webmention.io/">webmention.io</a> and the timestamp of the latest, so that the next one only asks for new webmentions.</li> </ul> <p>It puts less pressure on <a href="http://webmention.io/">webmention.io</a>, with one single request per build, when a client implementation makes a much larger request (or even several, with pagination) for <strong>each</strong> page view.</p> <p>For example:</p> <ul> <li>my website received 75 webmentions in April 2020. I have probably built it a hundred times during the same period, so let's say <strong>100 requests to <a href="http://webmention.io/">webmention.io</a> with small responses</strong>.</li> <li>in the same period, my website had 3,746 page views (underestimated, I still use Google Analytics đŸ€·â€â™‚ïž), which would have made <strong>3,746 requests to <a href="http://webmention.io/">webmention.io</a> with large responses</strong>.</li> </ul> <p>Using the server-side build to get the webmentions provides multiple benefits:</p> <ul> <li>The performance for the users is much better, with HTML already computed on the server and statically served.</li> <li>Much fewer API calls, requiring much less computing time and power.</li> <li>Everyone should know that <a href="https://aaronparecki.com/">Aaron Parecki</a> provides the awesome <a href="http://webmention.io/">webmention.io</a> service <strong>for free</strong>, and most Webmention users seem to use it nowadays, so being nice with its API feels better.</li> </ul> <div class="heading-wrapper"> <h2 id="enhance-client-side-if-really-needed" tabindex="-1">Enhance client-side, if really needed</h2> </div> <p>If you know you receive a lot of very useful webmentions that you have to show to your visitors, you can enhance the server-side generated list with a bit of client-side.</p> <p>But remember every JavaScript added to the page has a cost, so the few additional webmentions have to be really useful.</p> <p>So, instead of doing this for every page view, at least:</p> <p>First, try to <strong>wait for some time after the site build</strong> before making client-side API calls. Keep the build timestamp available to client-side JavaScript, and wait for an hour, a day, or more, depending on the frequency of webmentions. You could even use the page's &quot;age&quot; to query <a href="http://webmention.io/">webmention.io</a> less for older content that probably receives less webmentions, as <a href="https://aarongustafson.github.io/jekyll-webmention_io/performance-tuning">Aaron Gustafson did even for server-side call in his Jekyll plugin</a>.</p> <p>Then, <strong>keep track of a user's calls to the API</strong>, in localStorage or IndexedDB, so that you don't make these calls again a short while after. You could even use a Service Worker to cache requests and their timestamp.<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/#fn1" id="fnref1">[1]</a></sup></p> <div class="heading-wrapper"> <h2 id="client-side-only-api-calls-sometimes-make-more-sense" tabindex="-1">Client-side only API calls sometimes make more sense</h2> </div> <p>I agree Webmentions are not the most complex use case to explain that most of the time you should call APIs from the server at build time rather than from the client:</p> <ul> <li>Webmentions to show are the same for all visitors.</li> <li>Missing a few of the latest ones is probably not an issue.</li> </ul> <p>So yes, many other use cases make client-side API calls necessary, or better than server-side ones, I understand that.</p> <p>I say <strong>it should not be the default</strong>.</p> <div class="heading-wrapper"> <h2 id="promoting-the-aj-mstack-mstack" tabindex="-1">Promoting the <del>AJMstack</del> Mstack</h2> </div> <link rel="stylesheet" href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/styles.css" /> <p>That's also something I don't really like in current JAMstack trend, promoting <strong>J</strong>avaScript and <strong>A</strong>PIs much more than <strong>M</strong>arkup.</p> <p>Here's for example what you can see on <a href="https://jamstack.wtf/">jamstack.wtf</a> (simplified):</p> <dl class="stack stack-wtf"> <dt class="stack__name">JAMstack </dt><dd> <ol> <li class="stack__javascript">JavaScript</li> <li class="stack__apis">APIs</li> <li class="stack__markup">Markup</li> </ol> </dd> </dl> <p>As suggested by <a href="https://twitter.com/yann_yinn">Yann</a>, I would like to start by using this better presentation<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/#fn2" id="fnref2">[2]</a></sup>:</p> <dl class="stack stack-jam"> <dt class="stack__name">JAMstack </dt><dd> <ol> <li class="stack__javascript">JavaScript</li> <li class="stack__apis">APIs</li> <li class="stack__markup">Markup</li> </ol> </dd> </dl> <p>It makes more obvious there is a pile of things, quite useful for a &quot;stack&quot;.</p> <p>But I would like to suggest this modification:</p> <dl class="stack stack-ajm"> <dt class="stack__name">AJMstack </dt><dd> <ol> <li class="stack__apis">APIs</li> <li class="stack__javascript">JavaScript</li> <li class="stack__markup">Markup</li> </ol> </dd> </dl> <p>Of course, it reads as <strong>AJMstack</strong> instead of JAMstack, so I bet I won't be successful promoting it
 đŸ€·â€â™‚ïž</p> <p>But at least it feels more accurate, it shows JavaScript is the link between APIs and Markup.</p> <p>It even allows to present this as a great <a href="https://nicolas-hoizey.com/tags/progressive-enhancement/">progressive enhancement</a> platform, as we can start with plain old (did I hear &quot;boring&quot;?) Markup
</p> <p>Here's the <strong>Mstack</strong>:</p> <dl class="stack stack-m"> <dt class="stack__name">Mstack </dt><dd> <ol> <li class="stack__markup">Markup</li> </ol> </dd> </dl> <p>Make sure this &quot;stack&quot; is great, and then enhance with JavaScript and APIs.</p> <hr class="footnotes-sep" /> <section class="footnotes"> <ol class="footnotes-list"> <li id="fn1" class="footnote-item"><p><a href="https://twitter.com/bnijenhuis/">Bernard Nijenhuis</a> wrote about <a href="https://bnijenhuis.nl/notes/2021-07-07-implementing-service-workers-with-limited-cache/">how he handles a Cache of webmention.io requests with a Service Worker</a>. <a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/#fnref1" class="footnote-backref">↩</a></p> </li> <li id="fn2" class="footnote-item"><p>CSS Grid and Flexbox are so fun to use, it took me just a few minutes to get this, look at <a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/styles.css">this stylesheet</a>! đŸ’Ș <a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/#fnref2" class="footnote-backref">↩</a></p> </li> </ol> </section> Updating npm packages versions in package.json 2020-03-06T10:13:01Z https://nicolas-hoizey.com/articles/2020/03/06/updating-npm-packages-versions-in-package-json/ <div class="lead"> <p>I chose to use <a href="https://github.com/tjunnone/npm-check-updates"><code>npm-check-updates</code></a> to check for available updates of packages in my <code>package.json</code> files, and it always works without issues, so I guess I can recommend it.</p> <p>I'm also using this as a reminder for my own use
 😁</p> </div> <p>Here are the steps:</p> <div class="heading-wrapper"> <h2 id="install-npm-check-updates" tabindex="-1">Install <code>npm-check-updates</code></h2> </div> <p><code>npm-check-updates</code> is a <code>npm</code> package:</p> <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> <span class="token parameter variable">-g</span> npm-check-updates</code></pre> <div class="heading-wrapper"> <h2 id="check-for-available-updates" tabindex="-1">Check for available updates</h2> </div> <p>Run <code>ncu</code> (as in <strong>n</strong>pm-<strong>c</strong>heck-<strong>u</strong>pdate) to list updatable packages:</p> <pre class="language-bash"><code class="language-bash">ncu</code></pre> <div class="heading-wrapper"> <h2 id="automate-update-for-all-packages" tabindex="-1">Automate update for all packages</h2> </div> <p>If everything looks fine, update all package versions in your <code>package.json</code> file at once:</p> <pre class="language-bash"><code class="language-bash">ncu <span class="token parameter variable">-u</span></code></pre> <p>If you have doubts about at least one of the package versions, update your <code>package.json</code> file manually to test the results progressively.</p> <div class="heading-wrapper"> <h2 id="don-t-forget-to-actually-update-the-installed-packages" tabindex="-1">Don't forget to actually update the installed packages</h2> </div> <p><code>npm-check-updates</code> &quot;only&quot; updates the version numbers in your <code>package.json</code> file.</p> <p>You now have to really install them.</p> <p>Make sure current version of your <code>package-lock.json</code> file <a href="https://stackoverflow.com/a/44210813/717195">is versioned</a> (for any rollback need), then run:</p> <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span></code></pre> <div class="heading-wrapper"> <h2 id="what-about-salita" tabindex="-1">What about <code>salita</code>?</h2> </div> <p>There is also <a href="https://github.com/tbranyen/salita"><code>salita</code></a>, which has <a href="https://github.com/tjunnone/npm-check-updates/wiki/npm-check-updates-vs-salita">a few differences</a>.</p> <p>I prefer <code>npm-check-update</code> because its default behavior prevents accidental modification of the <code>package.json</code> file.</p> Can we monitor User Happiness on the Web with performance tools? 2020-01-10T10:13:01Z https://nicolas-hoizey.com/articles/2020/01/10/can-we-monitor-user-happiness-on-the-web-with-performance-tools/ <div class="lead"> <p>I really like that <a href="https://speedcurve.com/">SpeedCurve</a> tried to innovate with this recent &quot;<a href="https://support.speedcurve.com/docs/user-happiness">User Happiness</a>&quot; metric (<a href="https://web.archive.org/web/20201124064307/https://support.speedcurve.com/en/articles/3380780-user-happiness">original version</a>). It aggregates multiple technical metrics to decide if users visiting the page are happy or not with it. But I see several issues in this metric.</p> </div> <p><img src="https://nicolas-hoizey.com/articles/2020/01/10/can-we-monitor-user-happiness-on-the-web-with-performance-tools/speedcurve-user-happiness-monitoring.png" alt="User Happiness monitoring in SpeedCurve" title="User Happiness monitoring in SpeedCurve" class="zoom" /></p> <div class="heading-wrapper"> <h2 id="how-is-speed-curve-s-user-happiness-metric-computed" tabindex="-1">How is SpeedCurve's User Happiness metric computed?</h2> </div> <p>To be considered happy, a user navigation should validate all these technical metrics:</p> <blockquote> <ul> <li>start render &lt; 1100ms</li> <li>AND page load &lt; 3400ms</li> <li>AND DOM Content Loaded &lt; 1600ms</li> <li>AND First Contentful Paint &lt; 1200ms</li> <li>AND First Input Delay &lt; 8ms</li> <li>AND First CPU Idle &lt; 3900ms</li> <li>AND longest Long Task &lt; 380ms</li> <li>AND total CPU Time &lt; 1300ms</li> <li>
</li> </ul> </blockquote> <p>Even if performance is a key factor of happiness nowadays on the Web, the quality of the content and/or service provided by the page is obviously is even more important.</p> <p>There are <a href="https://duckduckgo.com/?q=measure+User+Happiness&amp;t=h_&amp;ia=web">a lot of publications on the Web about measuring user happiness</a>, most of which consider multiple user experience factors.</p> <p>I guess that's why SpeedCurve added two other metrics to the formula:</p> <blockquote> <ul> <li>
</li> <li>AND the user did not bounce</li> <li>AND the user did not abandon the page before it finished loading</li> </ul> </blockquote> <p>Interesting indeed.</p> <p>But I see several issues in this:</p> <div class="heading-wrapper"> <h2 id="users-browse-the-web-in-very-different-conditions" tabindex="-1">Users browse the Web in very different conditions</h2> </div> <p>For the technical performance values, SpeedCurve chose &quot;the thresholds [from] the median values across all of SpeedCurve's RUM data&quot;.</p> <p>But as Bruce Lawson showed us a few years ago, we should be talking about the <a href="https://www.smashingmagazine.com/2017/03/world-wide-web-not-wealthy-western-web-part-1/">World Wide Web, not Wealthy Westerners' Web</a> (read also <a href="https://www.smashingmagazine.com/2017/03/world-wide-web-not-wealthy-western-web-part-2/">part 2</a>).</p> <p>I think there are countries in the world where SpeedCurve's thresholds have no meaning at all, and users are very happy with much slower pages.</p> <p>These threshold should at least be based on regional data, but that might not be enough, because even in the same region, expectations about speed can vary depending on the service, usage conditions, etc.</p> <p>I don't believe it's possible to set such thresholds once and for all, people should be able to set their own thresholds.</p> <div class="heading-wrapper"> <h2 id="no-bounce-is-not-a-good-happiness-indicator" tabindex="-1">No bounce is not a good happiness indicator</h2> </div> <p>I believe adding bounce and abandonment metrics to User Happiness comes from a good intention to consider happiness not only as pure speed oriented, but it has its own bias.</p> <p>SpeedCurve tells us that</p> <blockquote> <p>bounced [is] the condition that contributes the most [to] the Unhappy page views</p> <p>In other words, no matter how well the other conditions perform, the number of Unhappy page views will always be 19% or higher.</p> </blockquote> <p>But while bounce is often seen as an issue, it might be preferable sometimes.</p> <p>When I'm looking for a precise information (store opening hours for example), I prefer coming directly from an external search to a page that contains the information, and bounce, than having to browse the site where the information is hard to find. In such scenarios, high bounce rate can be an indicator of good SEO, and user happiness. Even if the user abandon the page before it has finished loading, it might be because essential information was already available.</p> <p>I don't think it's a good idea to consider bounces as failure to make the user happy.</p> <p>So, as much as I'm happy people like SpeedCurve still try to find ways to monitor user happiness with automated technical tools, I think this iteration needs improvements. Keep up the good work!</p> How the Boeing 737 Max Disaster Looks to a Software Developer 2019-04-19T10:00:00Z https://nicolas-hoizey.com/articles/2019/04/19/how-the-boeing-737-max-disaster-looks-to-a-software-developer/ <div class="lead"> <p>Experienced plane pilot and software developer <a href="https://twitter.com/greg_travis">Gregory Travis</a> explains in details what led to <a href="https://en.wikipedia.org/wiki/Boeing_737_MAX">Boeing 737 Max</a> recent disasters in this long article: <a href="https://spectrum.ieee.org/aerospace/aviation/how-the-boeing-737-max-disaster-looks-to-a-software-developer">How the Boeing 737 Max Disaster Looks to a Software Developer</a>.</p> </div> <div class="heading-wrapper"> <h2 id="why-do-i-even-care" tabindex="-1">Why do I even care?</h2> </div> <p><strong>My family and I were in one of these</strong> Ethiopian Airlines' Boeing 737 Max just two weeks before <a href="https://en.wikipedia.org/wiki/Ethiopian_Airlines_Flight_302">the crash of flight 302</a>, on the same flight from Addis Ababa to Nairobi!</p> <p>The one that crashed was registered <a href="https://aviation-safety.net/database/record.php?id=20190310-0">ET-AVJ</a>. The one we took was registered ET-AVI<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2019/04/19/how-the-boeing-737-max-disaster-looks-to-a-software-developer/#fn1" id="fnref1">[1]</a></sup>. Very close. I guess both <del>have</del> had the very same hardware and software.</p> <p>It gives me chills every time I think about it.</p> <div class="heading-wrapper"> <h2 id="so-what-is-it-about" tabindex="-1">So, what is it about?</h2> </div> <p>I don't know much about planes, but this article explains everything very well. <strong>You should read it all</strong>, but here are some quotes (emphases are mine):</p> <blockquote> <p>In the 737 Max, the engine nacelles themselves can, at high angles of attack, work as a wing and produce lift. And the lift they produce is well ahead of the wing’s center of lift, meaning the nacelles will cause the 737 Max at a high angle of attack to go to a <em>higher</em> angle of attack. This is <strong>aerodynamic malpractice of the worst kind</strong>.</p> </blockquote> <blockquote> <p>The airframe, the hardware, should <strong>get it right the first time and not need a lot of added bells and whistles to fly predictably</strong>. This has been <a href="https://en.wikipedia.org/wiki/KISS_principle">an aviation canon</a> from the day the Wright brothers first flew at Kitty Hawk.</p> </blockquote> <blockquote> <p>the flight management computer can put a <em>lot</em> of force into that [pilot’s control] column—indeed, so much force that a human pilot can quickly become exhausted trying to pull the column back, <strong>trying to tell the computer that this really, really should not be happening</strong>.</p> </blockquote> <blockquote> <p>Those lines of code were no doubt created by people at the direction of managers. <strong>Neither such coders nor their managers are as in touch with the particular culture and mores of the aviation world as much as the people who are down on the factory floor</strong>, riveting wings on, designing control yokes, and fitting landing gears.</p> </blockquote> <p><img src="https://nicolas-hoizey.com/articles/2019/04/19/how-the-boeing-737-max-disaster-looks-to-a-software-developer/HAL9000.png" alt="" title="“2001, A Space Odyssey”'s HAL9000 rogue computer" class="onefourth" /></p> <blockquote> <p>Like someone with narcissistic personality disorder, MCAS (Maneuvering Characteristics Augmentation System) gaslights the pilots. And <strong>it turns out badly for everyone</strong>. “Raise the nose, HAL.” “I’m sorry, Dave, I’m afraid I can’t do that.”</p> </blockquote> <blockquote> <p>I believe the relative ease—not to mention the lack of tangible cost—of software updates has created <strong>a cultural laziness within the software engineering community</strong>. Moreover, because more and more of the hardware that we create is monitored and controlled by software, that cultural laziness is now creeping into hardware engineering—like building airliners. <strong>Less thought is now given to getting a design correct and simple up front because it’s so easy to fix what you didn’t get right later</strong>.</p> </blockquote> <p><strong>This is infuriating!</strong> These people gamble with human lives.</p> <p>Let's try at least to learn from our mistakes and get some good advice out of it
</p> <div class="heading-wrapper"> <h2 id="so-why-do-i-really-care" tabindex="-1">So, why do I really care?</h2> </div> <p>Apart from the fact that my family and I might have been in this crashed plane, I also care because I know there are similar issues everywhere in the industry, including software development.</p> <p>This is not fate, this is a consequence of a chain of bad decisions (or lack of). Considering the number of people involved, it should never have happened. But there is (a lot of) money involved. And lazyness.</p> <p>We often say, at least in software development, that laziness is a virtue. I believe it's not.</p> <p>Improving our processes, automating repetitive tasks, is beneficial for the quality of what we produce. It lowers the hassle caused by some of our tasks, which laziness would make us “forget” sooner or later. So laziness is not the virtue that makes us improve this, it's the vice we have to fight.</p> <p>The real virtue is in the efforts produced to compensate this lazyness.</p> <div class="heading-wrapper"> <h2 id="keep-it-simple-stupid" tabindex="-1">Keep It Simple, Stupid</h2> </div> <p>The amount of efforts required depends on the complexity of what we want to achieve, and how we plan to achieve it. If we plan for something really complicated, and imagine convoluted solutions to achieve it, we get exponential complexity.</p> <p>I always talk about the <a href="https://en.wikipedia.org/wiki/KISS_principle">KISS principle</a> when I teach software architecture and development. Several times a day.</p> <p>I will definitely add a quote from Gregory Travis' article in my slides:</p> <blockquote> <p>Every increment, every increase in complexity, ultimately leads to decreasing rates of return and, finally, to negative returns. Trying to patch and then repatch such a system in an attempt to make it safer can end up making it less safe.</p> </blockquote> <p>Similarly, <a href="https://en.wikipedia.org/wiki/Ray_Ozzie">Ray Ozzie</a>, once CTO of Microsoft, and previously creator of Lotus Notes<sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2019/04/19/how-the-boeing-737-max-disaster-looks-to-a-software-developer/#fn2" id="fnref2">[2]</a></sup>, <a href="https://www.azquotes.com/quote/585933">once said</a>:</p> <blockquote> <p><strong>Complexity kills.</strong> It sucks the life out of developers, it makes products difficult to plan, build and test, <strong>it introduces security challenges</strong>, and it causes end-user and administrator frustration.</p> </blockquote> <p><a href="https://en.wikipedia.org/wiki/Tony_Hoare">Tony Hoare</a>, the British computer scientist who developed quicksort, the sorting algorithm every developer learns sooner or later, also <a href="https://en.wikiquote.org/wiki/C._A._R._Hoare#The_Emperor's_Old_Clothes">said</a>:</p> <blockquote> <p>There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.</p> </blockquote> <p>We need to make things simple so that our lazyness has less opportunities to lead us to make mistakes.</p> <div class="heading-wrapper"> <h2 id="updates" tabindex="-1">Updates</h2> </div> <ul> <li>February 21st 2019, a reader <a href="https://www.air-journal.fr/2019-02-21-transavia-une-annee-record-des-737-max-a-lhorizon-5210570.html#comment-413823">comments a French article about Transavia potentialy buying 100 Boeing 737 MAX</a>: <blockquote> <p>This all 737 policy is not very good at all. The competitive process is always preferable. Imagine if this aircraft is grounded for one reason or another, it's a disaster for TO and others
</p> </blockquote> </li> <li>May 5th, 2019: <a href="https://www.wsj.com/articles/boeing-knew-about-safety-alert-problem-for-a-year-before-telling-faa-airlines-11557087129">Boeing Knew About Safety-Alert Problem for a Year Before Telling FAA, Airlines</a> (Wall Street Journal)</li> <li>June 28th, 2019: <a href="https://www.bloomberg.com/news/articles/2019-06-28/boeing-s-737-max-software-outsourced-to-9-an-hour-engineers">Boeing’s 737 Max Software Outsourced to $9-an-Hour Engineers</a> (Bloomberg)</li> <li>October 18th, 2019: <a href="https://www.cnbc.com/2019/10/18/boeing-shares-slide-on-report-faa-is-concerned-it-was-misled-about-737-max.html">Boeing lead pilot warned about flight-control system tied to 737 Max crashes, then told regulators to delete it from manuals</a> (CNBC)</li> <li>December 11th, 2019: <a href="https://www.ft.com/content/04f6f45e-1c2c-11ea-97df-cc63de1d73f4">US regulator failed to ground Boeing 737 Max despite risks</a> (Financial Times)</li> <li>January 10th, 2020: <a href="https://www.bloomberg.com/opinion/articles/2020-01-10/-designed-by-clowns-and-supervised-by-monkeys-the-737-max-story">‘Designed by Clowns and Supervised by Monkeys’: The 737 Max Story</a> (Bloomberg)</li> <li>January 14th, 2020: <a href="https://www.bloomberg.com/news/articles/2020-01-14/lion-air-idiots-sought-more-max-training-boeing-thwarted-it">Boeing Mocked Lion Air Calls for More 737 Max Training Before Crash</a> (Bloomberg)</li> <li>January 20th, 2020: <a href="https://www.nytimes.com/2020/01/20/business/boeing-737-accidents.html">How Boeing’s Responsibility in a Deadly Crash ‘Got Buried’</a> (The New York Times)</li> </ul> <hr class="footnotes-sep" /> <section class="footnotes"> <ol class="footnotes-list"> <li id="fn1" class="footnote-item"><p>Thanks <a href="https://my.flightradar24.com/nhoizey">myFlightradar24</a> for the information
 <a href="https://nicolas-hoizey.com/articles/2019/04/19/how-the-boeing-737-max-disaster-looks-to-a-software-developer/#fnref1" class="footnote-backref">↩</a></p> </li> <li id="fn2" class="footnote-item"><p>Well
 maybe not a good idea to keep it in his resume
 <a href="https://nicolas-hoizey.com/articles/2019/04/19/how-the-boeing-737-max-disaster-looks-to-a-software-developer/#fnref2" class="footnote-backref">↩</a></p> </li> </ol> </section> 100000 tweets 2018-10-24T10:00:00Z https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/ <div class="lead"> <p>100000 tweets. Already! Only? A little more than 11 years on Twitter, since I signed up May 25th 2007, and today I post my 100000th tweet. A lot have changed on Twitter, and it has brought me a lot of great things.</p> </div> <p><a href="https://twitter.com/nhoizey/status/1055058216298651650">https://twitter.com/nhoizey/status/1055058216298651650</a></p> <p>My <strong>first tweet ever, on May 25th, 2007</strong>, was unfortunately as dumb as first tweets of many others:</p> <p><a href="https://twitter.com/nhoizey/status/77754112">https://twitter.com/nhoizey/status/77754112</a></p> <p>And it amazes me today that <strong>the second tweet only came 10 months later</strong><sup class="footnote-ref"><a href="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/#fn1" id="fnref1">[1]</a></sup>, on April 4th, 2008, and was there to complain in the void:</p> <p><a href="https://twitter.com/nhoizey/status/782656417">https://twitter.com/nhoizey/status/782656417</a></p> <p>Over the years, I've discovered many great people thanks to Twitter, met a good portion of them, and some even became friends and/or colleagues.</p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-is-over-capacity.png" alt="Twitter over capacity whale" class="onethird" /></p> <p>I've also learned a lot, easily followed Web trends and innovations, and quickly found accurate answers and assistance when I had issues or general questions, mostly about Web technologies.</p> <p>Twitter really changed the way I consume news. Sorry RSS and Atom, <a href="https://nicolas-hoizey.com/atom.xml">I still love you</a>, but it looks like most others now prefer feeding their Twitter accounts.</p> <p>With 4171 days since my first tweet, I have published 24 tweets a day on average. But there have been empty days of course, and many really busier days, with long discussions and fierce debates about Web standards, dumb practices on the Web or IRL, joyful threads full of jokes, etc.</p> <p>There have been some hiccups, clashes, of course, that's life.</p> <p>Regarding <a href="https://twitter.com/nhoizey/following">accounts I follow</a>, I tried to <a href="https://twitter.com/search?q=from%3Anhoizey%20%23keepItTo200&amp;src=typed_query&amp;f=live">limit the number to 200</a> for a while, to limit the volume of tweets on my timeline, but it looks like I failed after this last successful attempt 2 years ago:</p> <p><a href="https://twitter.com/nhoizey/status/793955633733189632">https://twitter.com/nhoizey/status/793955633733189632</a></p> <p>A large part of my followees fortunately don't tweet that much, and I keep them because they are friends, colleagues, or they publish really interesting tweets from time to time. I should start thinking about daily tweets on my timeline instead of number of followees.</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/Pxc7vXq56VJm0/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/Pxc7vXq56VJm0/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/Pxc7vXq56VJm0/giphy.gif">the animated GIF</a>.</p></video></div> <p>Overall, these have been 11 great years, but bad practices from Twitter on <a href="https://www.vox.com/culture/2018/9/20/17876098/twitter-chronological-timeline-back-finally">the interface</a>, <a href="http://apps-of-a-feather.com/">the platform</a> and <a href="https://www.fastcompany.com/40547818/did-we-create-this-monster-how-twitter-turned-toxic">the content</a> have help great open source, distributed and federated alternatives such as <a href="https://en.wikipedia.org/wiki/Mastodon_(software)">Mastodon</a> thrive.</p> <p>Even if I don't have as many contacts and interesting discussions there yet, I'm happy to be part of the movement towards this better platform. You can find me at <a href="https://mamot.fr/@nhoizey">@nhoizey@mamot.fr</a>.</p> <p>I will continue using Twitter, because it's still my main source of technical news, I quickly find some help when I need it, and I have a lot of friends there that are not yet on Mastodon. But I hope I will be able to slowly migrate.</p> <p>Here are a few screenshots of Twitter along the years:</p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2008-crop.png" alt="" title="My Twitter profile in 2008 ([full page](twitter-2008-full-page.png))" /></p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2009-crop.png" alt="" title="My Twitter profile in 2009 ([full page](twitter-2009-full-page.png))" /></p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2010-crop.png" alt="" title="My Twitter profile in 2010 ([full page](twitter-2010-full-page.png))" /></p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2013-crop.png" alt="" title="My Twitter profile in 2013 ([full page](twitter-2013-full-page.png))" /></p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2014-crop.png" alt="" title="My Twitter profile in 2014 ([full page](twitter-2014-full-page.png))" /></p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2015-crop.jpg" alt="" title="My Twitter profile in 2015 ([full page](twitter-2015-full-page.jpg))" /></p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2016-crop.jpg" alt="" title="My Twitter profile in 2016 ([full page](twitter-2016-full-page.jpg))" /></p> <p><img src="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/twitter-2018-crop.jpg" alt="" title="My Twitter profile in 2018 ([full page](twitter-2018-full-page.jpg))" /></p> <p>Let's go for the next 100000 tweets!</p> <hr class="footnotes-sep" /> <section class="footnotes"> <ol class="footnotes-list"> <li id="fn1" class="footnote-item"><p>You can easily find your early tweets with Twitter Search. Here are <a href="https://twitter.com/search?q=from%3Anhoizey%20since%3A2007-05-01%20until%3A2008-04-10&amp;src=typed_query&amp;f=live">my tweets from May 1st, 2007 to April 10th, 2008</a> <a href="https://nicolas-hoizey.com/articles/2018/10/24/100000-tweets/#fnref1" class="footnote-backref">↩</a></p> </li> </ol> </section> The treasure behind the castle walls 2018-10-02T10:00:00Z https://nicolas-hoizey.com/articles/2018/10/02/the-treasure-behind-the-castle-walls/ <div class="lead"> <blockquote> <p>Pile up enough treasure behind the castle walls and you'll eventually attract someone who can climb them.</p> </blockquote> </div> <p><img src="https://nicolas-hoizey.com/articles/2018/10/02/the-treasure-behind-the-castle-walls/Maciej-Ceglowski-republica.jpg" alt="A photo of Maciej Ceglowski" title="Maciej Ceglowski giving a talk at re:publica 2017" class="onethird" /></p> <p>This is a nice quote about security threats caused by concentrating our personal data in a few centralised services, from a great talk <a href="https://en.wikipedia.org/wiki/Maciej_Ceg%C5%82owski">Maciej Ceglowski</a>, founder of <a href="https://nicolas-hoizey.com/tags/pinboard/">Pinboard</a> (the &quot;social bookmarking site for introverts&quot; <a href="https://nicolas-hoizey.com/2014/12/mes-bookmarks-migrent-de-diigo-vers-pinboard.html">I chose a while ago</a>), gave at <a href="https://re-publica.com/en">re:publica</a> in 2017.</p> <p>Read <a href="http://idlewords.com/talks/notes_from_an_emergency.htm">the full transcript of the &quot;Notes from an Emergency&quot; talk</a> or see <a href="https://www.youtube.com/watch?v=rSrLjb3k1II">the video</a>:</p> <p><a href="https://youtu.be/rSrLjb3k1II">https://youtu.be/rSrLjb3k1II</a></p> Using Cloudinary to convert an animated GIF to a video 2018-08-01T10:00:00Z https://nicolas-hoizey.com/articles/2018/08/01/using-cloudinary-s-fetch-api-to-convert-an-animated-gif-to-a-video/ <div class="lead"> <p>I like animated GIFs, like most people these days I think, but they are really heavy, hurting the performance of web pages, and consuming data plans faster than should be needed. So we need to convert them to videos, which are much lighter, for the same visual result. Let's use Cloudinary.</p> </div> <p><img src="https://nicolas-hoizey.com/assets/logos/cloudinary.png" alt="The Cloudinary logo" class="logo" /></p> <p>The animations in this post are animated GIFs provided by Giphy and (obviously) converted by Cloudinary.</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif">the animated GIF</a>.</p></video></div> <div class="info"> <p>You need a Cloudinary account to try all of this, but no fear, the <strong>free plan</strong> is really confortable for personnal use or just testing: <a href="https://nho.link/cloudinary-signup">signup for Cloudinary</a>.</p> </div> <div class="heading-wrapper"> <h2 id="so-what-s-the-issue-with-animated-gi-fs" tabindex="-1">So, what's the issue with animated GIFs?</h2> </div> <p>Animated GIFs are really popular, thanks to nice cinemagraphs, or funny reaction images. But all these animated GIFs tend to be really really heavy.</p> <p>This means:</p> <ul> <li>once the animated GIF starts downloading and rendering, it will increase the load on the browser CPU and memory and make it less responsive</li> <li>the user will consume her data-plan faster</li> </ul> <div class="heading-wrapper"> <h2 id="what-s-the-solution" tabindex="-1">What's the solution?</h2> </div> <p>Current state of the art for dealing with heavy animations is to replace animated GIFs with videos, which are much lighter. Twitter <a href="https://mashable.com/2014/06/20/twitter-gifs-mp4/">has been doing for years</a>, for example. Giphy even provides MP4 alternatives to their animated GIFs.</p> <p>Videos decoding can even be done by the device GPU, which means the CPU will not suffer like with animated GIFs, and the rendering will be smooth.</p> <div class="heading-wrapper"> <h2 id="what-about-the-easy-autoplay-of-animated-gi-fs" tabindex="-1">What about the easy autoplay of animated GIFs?</h2> </div> <p>Inline videos without a sound track, or muted, can be set to autoplay since:</p> <ul> <li><a href="https://developers.google.com/web/updates/2016/07/autoplay">Chrome for Android 53</a> (2016)</li> <li><a href="https://webkit.org/blog/6784/new-video-policies-for-ios/">iOS 10</a> (also 2016)</li> </ul> <div class="heading-wrapper"> <h2 id="ok-so-which-video-format" tabindex="-1">Ok, so which video format?</h2> </div> <p>We only need one video format, MP4 encoded as <code>H.264/AAC</code>, because (almost) <a href="https://caniuse.com/#feat=mpeg4">all browsers now support it</a>:</p> <figure><img src="https://caniuse.bitsofco.de/image/mpeg4.png" alt="Browser support for feature “mpeg4“" width="800" /><figcaption><a href="https://caniuse.com/#feat=mpeg4">Can I Use mpeg4?</a></figcaption></figure> <p>But adding WebM encoded as <code>VP9/Opus</code> can save a few more bytes, which is what we try to do here. And WebM support is pretty good:</p> <figure><img src="https://caniuse.bitsofco.de/image/webm.png" alt="Browser support for feature “webm“" width="800" /><figcaption><a href="https://caniuse.com/#feat=webm">Can I Use webm?</a></figcaption></figure> <p>If you encode and host video files yourself, you could choose to use only MP4, to ease video creation and lower storage capacity requirements.</p> <p>But if you don't have these concerns, for example because you use a CDN where storage is cheap and costs depend more on bandwidth, you should use both formats.</p> <div class="heading-wrapper"> <h2 id="how-can-cloudinary-help" tabindex="-1">How can Cloudinary help?</h2> </div> <p>Cloudinary has been providing animated GIFs to video conversion for a while, as this blog post from 2014 shows: <a href="https://cloudinary.com/blog/reduce_size_of_animated_gifs_automatically_convert_to_webm_and_mp4">Reduce size of animated GIFs, automatically convert to WebM and MP4</a>.</p> <p>Here is the simple process it described:</p> <p>First upload the animated GIF to Cloudinary, so that it is available at this URL:</p> <pre><code>https://res.cloudinary.com/demo/image/upload/kitten_fighting.gif </code></pre> <p>Then, change the file extension at the end of the URL to ask Cloudinary to convert it into WebM or MP4 video:</p> <pre><code>https://res.cloudinary.com/demo/image/upload/kitten_fighting.webm https://res.cloudinary.com/demo/image/upload/kitten_fighting.mp4 </code></pre> <p>Easy! Magical!</p> <p>But <strong>I want my publication process to be even easier</strong>, not requiring any upload (manual or automated) of my digital assets.</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/C41yP1w3Pe0la/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/C41yP1w3Pe0la/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/C41yP1w3Pe0la/giphy.gif">the animated GIF</a>.</p></video></div> <p>That's why I find <a href="https://cloudinary.com/documentation/fetch_remote_images#remote_image_fetch_url">Cloudinary's Fetch API</a> awesome!</p> <p>You can also use Cloudinary's Auto-Upload, which provides <a href="https://cloudinary.com/documentation/fetch_remote_images">a lot more features</a>, but I like to keep things simple, and my master pristine images are hosted on my site anyway. Actually, <a href="https://nhoizey.github.io/jekyll-cloudinary/">my Jekyll-Cloudinary plugin</a> uses the Fetch API to provide simple and efficient responsive images to Jekyll users.</p> <p>So, how can we use the Fetch API to convert animated GIFs to videos?</p> <p>Let's say the pristine animated GIF is located at <code>https://example.com/anim.gif</code>.</p> <p>The simple Fetch API URL to serve this image though Cloudinary, but untouched, would be this:</p> <pre><code>https://res.cloudinary.com/&lt;cloud_name&gt;/image/fetch/https://example.com/anim.gif </code></pre> <p><code>&lt;cloud_name&gt;</code> should be replaced by your own <a href="https://cloudinary.com/documentation/solution_overview#cloud_name">cloud_name</a>.</p> <p>If we try to replace <code>.gif</code> with <code>.mp4</code> at the end of this URL, like in the 4 years old Cloudinary post, it won't work, because Cloudinary will try to fetch a video located at <code>https://example.com/anim.mp4</code>, which doesn't exist.</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/5yeHSK4yNQAy4/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/5yeHSK4yNQAy4/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/5yeHSK4yNQAy4/giphy.gif">the animated GIF</a>.</p></video></div> <p><strong>The solution</strong> is to use the <a href="https://cloudinary.com/documentation/image_transformations#image_format_support">explicit format conversion parameter (<code>f_</code>)</a> you can set in your Fetch URL, before the pristine image URL:</p> <pre><code>https://res.cloudinary.com/&lt;cloud_name&gt;/image/fetch/f_mp4/https://example.com/anim.gif </code></pre> <p>So, we can replace this:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/anim.gif<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>an animation<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre> <p>With this:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>video</span> <span class="token attr-name">autoplay</span> <span class="token attr-name">loop</span> <span class="token attr-name">muted</span> <span class="token attr-name">playsinline</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://res.cloudinary.com/&lt;cloud_name>/image/fetch/f_webm/https://example.com/anim.gif<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/webm<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://res.cloudinary.com/&lt;cloud_name>/image/fetch/f_mp4/https://example.com/anim.gif<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/mp4<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Your browser doesn't support HTML5 video, <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/anim.gif<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>download the animated GIF<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span>.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>video</span><span class="token punctuation">></span></span></code></pre> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/uKpWZU3VXLprW/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/uKpWZU3VXLprW/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/uKpWZU3VXLprW/giphy.gif">the animated GIF</a>.</p></video></div> <div class="heading-wrapper"> <h2 id="don-t-use-the-animated-gif-has-a-fallback" tabindex="-1">Don't use the animated GIF has a fallback</h2> </div> <p>Be careful if you put the source animated GIF has a <code>&lt;img&gt;</code> fallback for the <code>&lt;video&gt;</code> for browsers that don't support HTML5 videos, because it will always be downloaded!</p> <p>If you need to publish animated GIFs anyway, at least make sure they are not too heavy (500KB should really be a maximum IMHO). You can use tools like <a href="https://kornel.ski/lossygif">Lossy GIF</a> to optimize these animated GIFs.</p> <div class="heading-wrapper"> <h2 id="be-careful" tabindex="-1">Be careful!</h2> </div> <p>The transformation on Cloudinary can take some time if the animated GIF is really heavy, so you might have to consider uploading it and perform the transformation asynchronously, without using the Fetch API.</p> <p>I didn't find any of this explained in Cloudinary documentation, maybe because it mixes images and videos.</p> <div class="heading-wrapper"> <h2 id="one-more-thing" tabindex="-1">One more thing
</h2> </div> <p>If you want to dive deeper in this topic, you can discover how — in the near future — animated GIFs converted to videos could be better loaded in <code>&lt;img&gt;</code> tags, with <a href="https://twitter.com/colinbendell">Colin Bendell</a>'s post in the 2017 edition of Performance (Advent) Calendar: <a href="https://calendar.perfplanet.com/2017/animated-gif-without-the-gif/">Evolution of &lt;img&gt;: Gif without the GIF</a>.</p> <p>Apple has been the first to <a href="https://bugs.webkit.org/show_bug.cgi?id=176825">implement support of soundless videos has <code>src</code> attribute of <code>img</code> elements in Safari</a>.</p> <p>Chomium <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=791658">intended to implement it</a>, but it was closed as <code>WontFix</code>.</p> <div class="heading-wrapper"> <h2 id="additional-resources" tabindex="-1">Additional resources</h2> </div> <ul> <li><a href="https://justmarkup.com/log/2018/02/gifhancement/">Gifhancement – convert GIF to video and embed responsible</a> by <a href="https://justmarkup.com/log/servus-hello-and-welcome/">Michael Scharnagl</a></li> <li><a href="https://www.smashingmagazine.com/2018/11/gif-to-video/">Improve Animated GIF Performance With HTML5 Video</a> on Smashing Magazine</li> </ul> Lapeyre, y'en a pas deux. J'espĂšre ! 2018-07-17T10:00:00Z https://nicolas-hoizey.com/articles/2018/07/17/lapeyre-y-en-a-pas-deux-j-espere/ <div class="lead"> <p>Je ne sais pas si vous avez dĂ©jĂ  Ă©tĂ© confrontĂ© au SAV de Lapeyre, mais je peux vous dire qu'il vaut son pesant de cacahuĂštes.</p> </div> <p><img src="https://nicolas-hoizey.com/assets/logos/lapeyre.png" alt="" class="onethird" /></p> <p>Le <strong>4 octobre 2017</strong>, un technicien Lapeyre venait prendre des mesures chez moi pour avoir les dimensions sur mesure d'une porte de garage.</p> <p>Le <strong>11 janvier 2018</strong>, voilĂ  enfin le jour de l’installation ! ForcĂ©ment, ça a pris un peu de temps, puisqu'il fallait fabriquer la porte sur mesure (merci le promoteur, il y a 35 ans
) et qu'il y a eu les fĂȘtes de fin d'annĂ©e.</p> <p>Ah
 mais en fait, <strong>une des piĂšces livrĂ©es n’est pas aux bonnes dimensions</strong> !</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/3oEjHWzZQaCrZW2aWs/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/3oEjHWzZQaCrZW2aWs/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/3oEjHWzZQaCrZW2aWs/giphy.gif">the animated GIF</a>.</p></video></div> <p>Sympa, du coup la porte est certes installĂ©e, mais l'Ă©tanchĂ©itĂ© en haut et l'isolation ne sont pas opĂ©rantes. En plein hiver, c'est cool.</p> <p>Multiples Ă©changes avec le SAV (quand il est joignable, et avec un changement de numĂ©ro de tĂ©lĂ©phone en cours de route, c'est plus simple), pour dĂ©couvrir <strong>au bout de plusieurs semaines</strong> (on est en <strong>mars</strong>) que <strong>la nouvelle piĂšce n'a en fait pas encore Ă©tĂ© commandĂ©e</strong> !</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/XsUtdIeJ0MWMo/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/XsUtdIeJ0MWMo/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/XsUtdIeJ0MWMo/giphy.gif">the animated GIF</a>.</p></video></div> <p>Une fois enfin commandĂ©e, le SAV ne peut (ou veut ?) plus trop rien faire, il faut voir avec le magasin (ils ne se parlent pas, ce serait trop simple), notamment le service installations. Sauf qu'ils ne sont jamais joignables, et surtout n'auraient pas idĂ©e de me prĂ©venir si la piĂšce arrive enfin.</p> <p>Arrive <strong>avril</strong> (oui, dĂ©jĂ ), et une date de livraison pour la piĂšce aux bonnes dimensions. Enfin !</p> <p>Courant <strong>mai</strong>, la piĂšce est enfin lĂ , le SAV me dit de voir avec le service installation du magasin, qui ne rĂ©pond pas, alors qu'il ne reste « plus » qu'Ă  trouver une date de dispo pour l'installateur, un sous-traitant, que je ne peux bien sĂ»r pas contacter en direct. Simple.</p> <p>Le <strong>22 juin 2018</strong>, l'installateur vient enfin poser la piĂšce manquante, c'est fini, je suis tranquille !!!</p> <p><strong>Ah non, la piĂšce est incomplĂšte</strong>, il manque le joint, on ne peut pas la poser comme ça mon bon monsieur, vous comprenez
</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/TseBjMu53JgWc/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/TseBjMu53JgWc/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/TseBjMu53JgWc/giphy.gif">the animated GIF</a>.</p></video></div> <p>En ce <strong>17 juillet 2018, la pose de ma porte de garage n’est toujours pas finalisĂ©e</strong>.</p> <p>La nouvelle piĂšce, aux bonnes dimensions et avec un joint est commandĂ©e (c'est dĂ©jĂ  ça, j'ai quand mĂȘme un peu doutĂ©), mais aucune date de livraison n'est annoncĂ©e. On me dit gentiment, au service installation du magasin, que « ce sont bientĂŽt les vacances, donc ça va devenir compliqué » </p> <p><strong>CompliquĂ© ?</strong></p> <p><strong>COMPLIQUÉ ???</strong></p> <p>C'est pas un peu compliquĂ©, dĂ©jĂ , depuis <strong>10 mois que ça dure</strong> ?</p> <div class="giphy"><video controls="" loop="" muted="" playsinline="" preload="auto" crossorigin="anonymous"><source src="https://res.cloudinary.com/nho/image/fetch/f_webm/https://media.giphy.com/media/10UHehEC098kAE/giphy.gif" type="video/webm" /><source src="https://res.cloudinary.com/nho/image/fetch/f_mp4/https://media.giphy.com/media/10UHehEC098kAE/giphy.gif" type="video/mp4" /><p>Your browser doesn't support video. See <a href="https://media.giphy.com/media/10UHehEC098kAE/giphy.gif">the animated GIF</a>.</p></video></div> <p>Vous imaginez bien que je vais dire beaucoup de bien de Lapeyre autour de moi.</p> <p>Allez, je prends les paris pour la date de finalisation de mon installation
</p> <div class="heading-wrapper"> <h2 id="update-du-24-07-2018" tabindex="-1">Update du 24/07/2018</h2> </div> <p>Incroyable (ou pas), j'ai Ă©tĂ© contactĂ© rapidement par le (les ?) CM de Lapeyre, tant sur Twitter que Facebook, pour dire qu'ils s'occupaient de contacter tant le SAV que le magasin.</p> <p>Et 24h plus tard, l'installateur me disait qu'il avait la piĂšce, et me proposait de passer sous 2 jours.</p> <p>Bref, l'installation est enfin terminĂ©e.</p> <p>Incroyable comme il est possible d'ĂȘtre efficace, quand on commence Ă  subire un peu de pression avec du bad buzz
</p>