<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Nicolas Hoizey</title>
    <description>The latest articles on Forem by Nicolas Hoizey (@nhoizey).</description>
    <link>https://forem.com/nhoizey</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F88496%2F6398f630-a5b5-41b1-add3-55573420b093.jpeg</url>
      <title>Forem: Nicolas Hoizey</title>
      <link>https://forem.com/nhoizey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/nhoizey"/>
    <language>en</language>
    <item>
      <title>HTML and CSS based View Transitions are coming</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Fri, 19 May 2023 17:47:56 +0000</pubDate>
      <link>https://forem.com/nhoizey/html-and-css-based-view-transitions-are-coming-408l</link>
      <guid>https://forem.com/nhoizey/html-and-css-based-view-transitions-are-coming-408l</guid>
      <description>&lt;p&gt;While same-document View Transitions have now been &lt;a href="https://caniuse.com/?search=ViewTransition"&gt;available for a while&lt;/a&gt; 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 🤷‍♂️).&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's experiment!
&lt;/h2&gt;

&lt;p&gt;After following &lt;a href="https://developer.chrome.com/docs/web-platform/view-transitions/"&gt;Jake Archibald's work&lt;/a&gt; for many months now, and sharing &lt;a href="https://nicolas-hoizey.com/links/?tags=View%20Transitions"&gt;a few links about View Transitions&lt;/a&gt;, I wanted to try them, and decided &lt;a href="https://nicolas-hoizey.photo/"&gt;my photography site&lt;/a&gt; would be a good playground.&lt;/p&gt;

&lt;p&gt;Here's what I got with just &lt;a href="https://github.com/nhoizey/nicolas-hoizey.photo/blob/0ebfd123ab203c330dd24dc1abf2f0a068390b4e/src/_layouts/base.njk#L41"&gt;a &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag&lt;/a&gt; and &lt;a href="https://github.com/nhoizey/nicolas-hoizey.photo/blob/0ebfd123ab203c330dd24dc1abf2f0a068390b4e/assets/sass/_view-transitions.scss"&gt;a few CSS rules&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/Z_MG97DzNPs"&gt;https://youtu.be/Z_MG97DzNPs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks Dave Rupert for the &lt;a href="https://nicolas-hoizey.com/links/2023/05/19/getting-started-with-view-transitions-on-multi-page-apps/"&gt;very simple View Transitions tutorial&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://nicolas-hoizey.com/tags/jpeg-xl/"&gt;JPEG-XL&lt;/a&gt;…), 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ok for Chrome Canary, but elsewhere?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API"&gt;documentation for the View Transitions API on MDN&lt;/a&gt; is already available, but focused on SPA.&lt;/p&gt;

&lt;p&gt;Noam Rosenthal is currently leading the &lt;a href="https://github.com/WICG/view-transitions/pull/208"&gt;creation of a new explainer for "cross-document navigations" (MPA)&lt;/a&gt;, a welcome step towards standardisation, and maybe implementation in other browsers.&lt;/p&gt;

&lt;p&gt;There are also &lt;a href="https://github.com/w3c/csswg-drafts/labels/css-view-transitions-2"&gt;a lot of open issues in the CSS Working Group GitHub repository&lt;/a&gt; with ideas and questions.&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;I the mean time, I can only thank Jake Archibald A LOT for this nice improvement of the user experience! 🙏&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Running CSS animations only if both the device and the user allow it</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Fri, 07 Apr 2023 17:49:14 +0000</pubDate>
      <link>https://forem.com/nhoizey/running-css-animations-only-if-both-the-device-and-the-user-allow-it-5c5b</link>
      <guid>https://forem.com/nhoizey/running-css-animations-only-if-both-the-device-and-the-user-allow-it-5c5b</guid>
      <description>&lt;p&gt;Thanks to Chrome release notes, &lt;a href="https://nicolas-hoizey.com/notes/2023/04/07/1/"&gt;I discovered today&lt;/a&gt; that there is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/update-frequency"&gt;an &lt;code&gt;update&lt;/code&gt; media feature&lt;/a&gt; which accepts values &lt;code&gt;fast&lt;/code&gt;, &lt;code&gt;slow&lt;/code&gt; and &lt;code&gt;print&lt;/code&gt;, to set styles depending on the ability of the device to update the rendering and the speed of it.&lt;/p&gt;

&lt;p&gt;As I'm already respecting the user's preference with the &lt;code&gt;prefers-reduced-motion&lt;/code&gt; media feature, I wondered how I could progressively enhance this with the new media feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Testing" media feature support without &lt;code&gt;@supports&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://front-end.social/@AmeliaBR"&gt;Amelia Bellamy-Royds&lt;/a&gt; had &lt;a href="https://front-end.social/@AmeliaBR/110158330793667431"&gt;the answer with a clever trick&lt;/a&gt;. Thanks Amelia! 🙏&lt;/p&gt;

&lt;p&gt;This is how you can apply styles only if a feature is supported by the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@media (thing: one), not (thing: one) {
  …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Indeed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;either the browser doesn't understand &lt;code&gt;thing: one&lt;/code&gt; and ignores both media queries&lt;/li&gt;
&lt;li&gt;or the browser understands &lt;code&gt;thing: one&lt;/code&gt; and this is either &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, so combining both matches all supporting browsers/contexts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running CSS animations only if both the device (either &lt;code&gt;update: fast&lt;/code&gt; not supported or &lt;code&gt;true&lt;/code&gt;) and the user allow it (&lt;code&gt;prefers-reduced-motion: no-preference&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Combining this trick with my already existing media queries for the &lt;code&gt;prefers-reduced-motion&lt;/code&gt; media feature requires a bit of code, but it's manageable.&lt;/p&gt;

&lt;p&gt;Here's the code I got for the Ken Burns animations running on &lt;a href="https://nicolas-hoizey.photo/"&gt;my photography site&lt;/a&gt; (with non relevant selectors cleaned up):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Without @​media update support
// Enable animations if no reduced motion preference
@media (prefers-reduced-motion: no-preference) {
  img {
    animation-play-state: running;
  }
}

// With @​media update support
@media (update: fast), not (update: fast) {
  // If screen update is fast (neither slow nor print)
  @media (update: fast) {
    // Enable animations if no reduced motion preference
    @media (prefers-reduced-motion: no-preference) {
      img {
        animation-play-state: running;
      }
    }
  }

  // If screen update is NOT fast (either slow or print)
  // Disable animations
  @media not (update: fast) {
    img {
      animation-play-state: paused;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Being able to nest media queries is great!&lt;/p&gt;

&lt;h2&gt;
  
  
  Later update: a simpler solution thanks to Amelia
&lt;/h2&gt;

&lt;p&gt;As &lt;a href="https://front-end.social/@AmeliaBR/110160694917595587"&gt;Amelia noticed when I shared this&lt;/a&gt; (I ❤️ her comment “both awesome &amp;amp; awful” 😅), the code can be much simpler, without media queries inception. 😅&lt;/p&gt;

&lt;p&gt;Here's what &lt;a href="https://front-end.social/@AmeliaBR/110160702621003752"&gt;Amelia suggests to write&lt;/a&gt;, and I agree it's much better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@media (prefers-reduced-motion: no-preference) {
  img {
    animation-play-state: running;
    /* turn animations on if user doesn't mind */
  }
}
@media not (update: fast) {
  img {
    animation-play-state: paused;
    /* except, turn them off again if the browser can't draw them effectively anyway */
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I feel so lucky to regularly get great feedback from people with such expertise as Amelia, on many topics. &lt;a href="https://nicolas-hoizey.com/links/2023/04/07/how-tweetbot-died-and-lived-again/"&gt;Once with Twitter, I now get this with Mastodon&lt;/a&gt;, and it feels even better.&lt;/p&gt;

&lt;p&gt;Now… how can I test the different permutations of &lt;code&gt;update&lt;/code&gt; support or not, and actual value? 🤔&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How I built my own excerpt for Markdown content in Eleventy</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Thu, 30 Mar 2023 23:40:32 +0000</pubDate>
      <link>https://forem.com/nhoizey/how-i-built-my-own-excerpt-for-markdown-content-in-eleventy-oa1</link>
      <guid>https://forem.com/nhoizey/how-i-built-my-own-excerpt-for-markdown-content-in-eleventy-oa1</guid>
      <description>&lt;p&gt;I was not really happy with &lt;a href="https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content"&gt;Eleventy's native excerpt solution&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;To be able to style the content lead whatever it contains, I'm using the great and simple &lt;a href="https://github.com/GerHobbelt/markdown-it-container#readme"&gt;&lt;code&gt;markdown-it-container&lt;/code&gt;&lt;/a&gt; plugin for Markdown-it, with a &lt;code&gt;lead&lt;/code&gt; container.&lt;/p&gt;

&lt;p&gt;For exemple, I can write this in the begining of my Markdown file, after the YAML Front Matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;::: lead
This paragraph is in the lead.

This other paragraph is also in the lead.
:::

This is no more part of the content lead…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this really simple syntax, I can put whatever I want in the lead and style it in the content page.&lt;/p&gt;

&lt;p&gt;And I can also extract it for the excerpt!&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Fortunately, Eleventy provides the &lt;code&gt;eleventyConfig.setFrontMatterParsingOptions()&lt;/code&gt; function which allows passing options to &lt;a href="https://github.com/jonschlinkert/gray-matter#readme"&gt;&lt;code&gt;gray-matter&lt;/code&gt;&lt;/a&gt;, the npm package it relies on to parse front matter.&lt;/p&gt;

&lt;p&gt;This is the function that allows setting a custom separator for the default excerpt feature with the &lt;a href="https://github.com/jonschlinkert/gray-matter#optionsexcerpt_separator"&gt;&lt;code&gt;excerpt_separator&lt;/code&gt;&lt;/a&gt; option, &lt;a href="https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content"&gt;as shown in Eleventy documentation&lt;/a&gt;, but in addition to the simple &lt;code&gt;true&lt;/code&gt; boolean shown here, the &lt;code&gt;excerpt&lt;/code&gt; option can be a function, which gets the Markdown content and options as parameters.&lt;/p&gt;

&lt;p&gt;Here's the function I built to generate my own excerpt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const markdownItPlainText = require('markdown-it-plain-text');
const excerptMd = new markdownIt().use(markdownItPlainText);

function grayMatterExcerpt(file, options) {
  const regex = /^.*::: lead(((?!(:::)).|\n)+):::.*$/gm;
  let excerpt = '';
  let leadFound = false;

  if ((leadMatches = regex.exec(file.content)) !== null) {
    lead = leadMatches[1];
    leadFound = true;
    excerptMd.render(lead);
  } else {
    excerptMd.render(file.content);
  }
  excerpt = excerptMd.plainText
    .trim()
    .replace(/{%(((?!(%})).|\n)+)%}/gm, '') // remove short codes
    .replace(/{{(((?!(}})).|\n)+)}}/gm, '') // remove nunjucks variables
    .replace(/{#(((?!(#})).|\n)+)#}/gm, '') // remove nunjucks comments
    .replace(/&amp;lt;style&amp;gt;(((?!(&amp;lt;\/style&amp;gt;)).|\n)+)&amp;lt;\/style&amp;gt;/gm, '') // remove inline CSS
    .replace(
      /&amp;lt;script type="application\/ld\+json"&amp;gt;(((?!(&amp;lt;\/script&amp;gt;)).|\n)+)&amp;lt;\/script&amp;gt;/gm,
      ''
    ) // remove JSON+LD
    .replace(/(&amp;lt;\/h[1-6]&amp;gt;)/gm, '. $1') // add a dot at the end of headings
    .replace(/&amp;lt;\/?([a-z][a-z0-9]*)\b[^&amp;gt;]*&amp;gt;|&amp;lt;!--[\s\S]*?--&amp;gt;/gm, '') // remove HTML tags
    .replace(/(\[\^[^\]]+\])/gm, '') // remove Markdown footnotes
    .replace(/\[([^\]]+)\]\(\)/gm, '$1') // remove Markdown links without URL (from {% link_to %} for example)
    .replace(/ +(\.|,)/gm, '$1'); // remove space before punctuation

  if (!leadFound &amp;amp;&amp;amp; excerpt.length &amp;gt; 150) {
    // Keep only 145 characters and an ellipsis if there was no declared lead
    excerpt = excerpt.replace(/^(.{145}[^\s]*).*/gm, '$1') + '…';
  }
  file.excerpt = excerpt;
}

eleventyConfig.setFrontMatterParsingOptions({
  excerpt: grayMatterExcerpt,
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It first looks for a &lt;code&gt;::: lead&lt;/code&gt; container to use directly, or takes the full content.&lt;/p&gt;

&lt;p&gt;It then parses the result with Markdown-it and the &lt;a href="https://github.com/wavesheep/markdown-it-plain-text#readme"&gt;&lt;code&gt;markdown-it-plain-text&lt;/code&gt;&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;Finally, if the full content was used (no &lt;code&gt;::: lead&lt;/code&gt; found), it limits the result length.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A bookmarklet to create a new link content Markdown on GitHub</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Wed, 08 Feb 2023 09:42:51 +0000</pubDate>
      <link>https://forem.com/nhoizey/a-bookmarklet-to-create-a-new-link-content-markdown-on-github-318</link>
      <guid>https://forem.com/nhoizey/a-bookmarklet-to-create-a-new-link-content-markdown-on-github-318</guid>
      <description>&lt;p&gt;When I was building my site on my local computer, I had a shell script to initialize a new Markdown file for sharing a &lt;a href="https://nicolas-hoizey.com/links/"&gt;link&lt;/a&gt;. When I &lt;a href="https://nicolas-hoizey.com/notes/2022/07/29/1/"&gt;moved to Cloudflare Pages 6 months ago&lt;/a&gt;, 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!&lt;/p&gt;

&lt;p&gt;The main features of my bookmarklet are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;get the page title, ask for any change in a &lt;code&gt;window.prompt()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;get some content

&lt;ul&gt;
&lt;li&gt;the selection made on current page,&lt;/li&gt;
&lt;li&gt;or the page's meta description,&lt;/li&gt;
&lt;li&gt;or the first paragraph of the first &lt;code&gt;main&lt;/code&gt; element,&lt;/li&gt;
&lt;li&gt;or the first paragraph of the first &lt;code&gt;article&lt;/code&gt; element,&lt;/li&gt;
&lt;li&gt;or the first paragraph of the page&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;compute the slug based on the title&lt;/li&gt;
&lt;li&gt;compute the file path for my links content type (&lt;code&gt;/links/YYYY/MM/DD/slug/index.md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;create the Markdown file content, with YAML Front Matter&lt;/li&gt;
&lt;li&gt;open a new file editor on GitHub, so I can add some content and metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I can then commit the file, push it directly to my &lt;code&gt;main&lt;/code&gt; branch or open a pull request.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://nicolas-hoizey.com/articles/2023/01/07/let-s-posse-to-mastodon-with-a-feed-and-a-github-action/"&gt;my GitHub Action&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;The JavaScript source code &lt;a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/main/assets/js/bookmarklets/new-link.js"&gt;is here on GitHub&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ==Bookmarklet==
// @name +🔗
// @description New link for nicolas-hoizey.com
// @version 1.0
// ==/Bookmarklet==

// Adapted from https://gist.github.com/codeguy/6684588#gistcomment-3361909
const slugify = (str) =&amp;gt; {
  let slug = str.toString();
  console.log(`1: ${slug}`);
  slug = slug.replaceAll('/', ' ');
  console.log(`2: ${slug}`);
  slug = slug.normalize('NFD');
  console.log(`3: ${slug}`);
  slug = slug.replace(/[\u0300-\u036f]/g, '');
  console.log(`4: ${slug}`);
  slug = slug.toLowerCase();
  console.log(`5: ${slug}`);
  slug = slug.replace(/\s+/g, ' ');
  console.log(`6: ${slug}`);
  slug = slug.replace(/[^\w]+/g, ' ');
  console.log(`7: ${slug}`);
  slug = slug.trim();
  console.log(`8: ${slug}`);
  slug = slug.replace(/ +/g, '-');
  console.log(`9: ${slug}`);

  return slug;
};

/* **********************************************************************************
/* Get data from the page
/* ********************************************************************************* /
let pageTitle = window.document.title;
let linkSelection =
  'getSelection' in window ? window.getSelection().toString().trim() : '';
let linkContent =
  linkSelection ||
  window.document
    .querySelector('head meta[name=description i]')
    ?.content.trim() ||
  window.document.querySelector('main p')?.textContent.trim() ||
  window.document.querySelector('article p')?.textContent.trim() ||
  window.document.querySelector('p')?.textContent.trim();
let linkUrl = window.location.href;

/* **********************************************************************************
/* Ask the user to confirm/modify the title
/* ********************************************************************************* /
let title = window.prompt('Title of the link?', pageTitle);

if (title !== null) {
  let slug = window.prompt('Slug of the link?', slugify(title));

  if (slug !== null) {
    /* **********************************************************************************
    /* Build the content
    /* ********************************************************************************* /
    const today = new Date();
    const dateString = today
      .toISOString()
      .replace('T', ' ')
      .replace(/\.[0-9]{3}Z/, ' +00:00');

    let value = `---
date: ${dateString}
title: "${title}"
lang: en
link: ${linkUrl}
authors:
  - ""
tags: []
---
\n
${linkContent ? `&amp;gt; ${linkContent.replaceAll('\n', '\n&amp;gt; ')}` : ''}
`;

    /* **********************************************************************************
    /* Build the URL
    /* ********************************************************************************* /
    const pathDate = dateString.slice(0, 10).replaceAll('-', '/');
    const filename = `src/links/${pathDate}/${slug}/index.md`;

    let newFileUrl = `https://github.com/nhoizey/nicolas-hoizey.com/new/main/?filename=${filename}&amp;amp;value=${encodeURIComponent(
      value
    )}`;

    window.open(newFileUrl);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metadata in the comment on top of this script is used by &lt;a href="https://www.npmjs.com/package/bookmarklet"&gt;bookmarklet&lt;/a&gt;, the npm package I use to transform my source JS into a proper bookmarklet, with &lt;a href="https://nicolas-hoizey.com/tools/bookmarklets/new-link"&gt;a page from which I can drag the link to my bookmarks bar&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bookmarklet --demo assets/js/bookmarklets/new-link.js src/tools/bookmarklets/new-link.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good old bookmarklets are still great in 2023! 🥰&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Updating webmentions on a static site</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Sun, 05 Feb 2023 19:27:38 +0000</pubDate>
      <link>https://forem.com/nhoizey/updating-webmentions-on-a-static-site-3b98</link>
      <guid>https://forem.com/nhoizey/updating-webmentions-on-a-static-site-3b98</guid>
      <description>&lt;p&gt;When &lt;a href="https://nicolas-hoizey.com/articles/2017/07/27/so-long-disqus-hello-webmentions/"&gt;I started using Webmention on this site&lt;/a&gt; (more than 5 years ago!), I was building the site on my local computer, and uploading the build result on my hosting with &lt;code&gt;rsync&lt;/code&gt;. I've &lt;a href="https://nicolas-hoizey.com/notes/2022/07/29/1/"&gt;moved to Cloudflare Pages 6 months ago&lt;/a&gt;, which means webmentions where updated only when I pushed new content to GitHub. Here's how I fixed that.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="http://webmention.io/"&gt;webmention.io&lt;/a&gt; API only ask for new mentions.&lt;/p&gt;

&lt;p&gt;Most of my Webmention implementation is based on two great inspiration sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://mxb.dev/"&gt;Max Böck&lt;/a&gt;'s &lt;a href="https://mxb.dev/blog/using-webmentions-on-static-sites/"&gt;Using Webmentions in Eleventy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sia.codes/"&gt;Sia&lt;/a&gt;'s &lt;a href="https://sia.codes/posts/webmentions-eleventy-in-depth/"&gt;An In-Depth Tutorial of Webmentions + Eleventy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's &lt;a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/main/.github/workflows/update-webmentions.yml"&gt;the workflow of my GitHub Action&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Check Webmentions
on:
  schedule:
    # Runs at every 15th minute from 0 through 59
    # https://crontab.guru/#0/15_*_*_*_*
    - cron: '0/15 * * * *'
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: true

jobs:
  webmentions:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout the project
        uses: actions/checkout@v2

      - name: Select Node.js version
        uses: actions/setup-node@v1
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm ci

      - name: Run webmention script
        env:
          WEBMENTION_IO_TOKEN: ${{ secrets.WEBMENTION_IO_TOKEN }}
        run: npm run webmention &amp;gt;&amp;gt; $GITHUB_STEP_SUMMARY

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v3
        with:
          token: ${{ secrets.PAT }}
          branch: webmentions
          delete-branch: true
          commit-message: Update Webmentions
          title: Update Webmentions
          labels: automerge 🤞

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses the &lt;code&gt;WEBMENTION_IO_TOKEN&lt;/code&gt; and &lt;code&gt;PAT&lt;/code&gt; (&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"&gt;Personal Access Token&lt;/a&gt;) secrets I've defined in my GitHub secrets for Actions.&lt;/p&gt;

&lt;p&gt;And here's &lt;a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/main/_scripts/update-webmention.js"&gt;the Node.js script that it runs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const fetch = require('node-fetch');
const unionBy = require('lodash/unionBy');
const sanitizeHTML = require('sanitize-html');
const domain = new URL(require('../package.json').homepage).hostname;
const { writeToCache, readFromCache } = require('../src/_utils/cache');

// Load .env variables with dotenv
require('dotenv').config();

// Define Cache Location and API Endpoint
const WEBMENTION_URL = 'https://webmention.io/api';
const WEBMENTION_CACHE = '_cache/webmentions.json';
const WEBMENTION_TOKEN = process.env.WEBMENTION_IO_TOKEN;

async function fetchWebmentions(since, perPage = 10000) {
  // If we dont have a domain name or token, abort
  if (!domain || !WEBMENTION_TOKEN) {
    console.warn('&amp;gt;&amp;gt;&amp;gt; unable to fetch webmentions: missing domain or token');
    return false;
  }

  let url = `${WEBMENTION_URL}/mentions.jf2?domain=${domain}&amp;amp;token=${WEBMENTION_TOKEN}&amp;amp;per-page=${perPage}`;
  if (since) url += `&amp;amp;since=${since}`; // only fetch new mentions

  const response = await fetch(url);
  if (!response.ok) {
    return null;
  }
  const feed = await response.json();
  const webmentions = feed.children;
  let cleanedWebmentions = cleanWebmentions(webmentions);
  if (cleanedWebmentions.length === 0) {
    console.log('[Webmention] No new webmention');
    return null;
  } else {
    console.log(`[Webmention] ${cleanedWebmentions.length} new webmentions`);
    return cleanedWebmentions;
  }
}

function cleanWebmentions(webmentions) {
  // https://mxb.dev/blog/using-webmentions-on-static-sites/#h-parsing-and-filtering
  const sanitize = (entry) =&amp;gt; {
    // Sanitize HTML content
    const { content } = entry;
    if (content &amp;amp;&amp;amp; content['content-type'] === 'text/html') {
      let html = content.html;
      html = html
        .replace(/&amp;lt;a [^&amp;gt;]+&amp;gt;&amp;lt;\/a&amp;gt;/gm, '')
        .trim()
        .replace(/\n/g, '&amp;lt;br /&amp;gt;');
      html = sanitizeHTML(html, {
        allowedTags: [
          'b',
          'i',
          'em',
          'strong',
          'a',
          'blockquote',
          'ul',
          'ol',
          'li',
          'code',
          'pre',
          'br',
        ],
        allowedAttributes: {
          a: ['href', 'rel'],
          img: ['src', 'alt'],
        },
        allowedIframeHostnames: [],
      });
      content.html = html;
    }

    // Fix missing publication date
    if (!entry.published &amp;amp;&amp;amp; entry['wm-received']) {
      entry.published = entry['wm-received'];
    }

    return entry;
  };

  return webmentions.map(sanitize);
}

// Merge fresh webmentions with cached entries, unique per id
function mergeWebmentions(a, b) {
  if (b.length === 0) {
    return a;
  }
  let union = unionBy(a, b, 'wm-id');
  union.sort((a, b) =&amp;gt; {
    let aDate = new Date(a.published || a['wm-received']);
    let bDate = new Date(b.published || b['wm-received']);
    return aDate - bDate;
  });
  return union;
}

const updateWebmention = async function () {
  const cached = readFromCache(WEBMENTION_CACHE) || {
    lastFetched: null,
    webmentions: [],
  };

  // Only fetch new mentions in production
  const fetchedAt = new Date().toISOString();
  const newWebmentions = await fetchWebmentions(cached.lastFetched);
  if (newWebmentions) {
    const webmentions = {
      lastFetched: fetchedAt,
      webmentions: mergeWebmentions(cached.webmentions, newWebmentions),
    };

    writeToCache(webmentions, WEBMENTION_CACHE);
  }
};

updateWebmention();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever the workflow updates the repository with new webmentions, it triggers a Cloudflare Pages build (could be Netlify), and the site is updated.&lt;/p&gt;

&lt;p&gt;It means I don't have to run a full build of the site periodically "just" to check if there are new webmentions, and the check can be more frequent, as it is really light and fast.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Let's POSSE to Mastodon with a Feed and a GitHub Action</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Sat, 07 Jan 2023 12:49:09 +0000</pubDate>
      <link>https://forem.com/nhoizey/lets-posse-to-mastodon-with-a-feed-and-a-github-action-1pp8</link>
      <guid>https://forem.com/nhoizey/lets-posse-to-mastodon-with-a-feed-and-a-github-action-1pp8</guid>
      <description>&lt;p&gt;After building a Node script &lt;a href="https://nicolas-hoizey.com/notes/2022/11/26/1/"&gt;for my own POSSE needs&lt;/a&gt;, 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!&lt;/p&gt;

&lt;p&gt;You should already know that I'm a true believer of &lt;a href="https://indieweb.org/"&gt;IndieWeb&lt;/a&gt; and &lt;a href="https://indieweb.org/POSSE"&gt;POSSE&lt;/a&gt;, as &lt;a href="https://nicolas-hoizey.com/archives/?query=indieweb"&gt;many contents I already published&lt;/a&gt; show.&lt;/p&gt;

&lt;p&gt;You could for example replace "Medium" with many other services, including Twitter and Mastodon, in &lt;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/"&gt;Medium is only an edge server of your POSSE CDN, your own blog is the origin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also gave a talk (in French) about IndieWeb and POSSE 3 years ago: &lt;a href="https://nicolas-hoizey.com/talks/2019/10/10/ne-vous-laissez-plus-deposseder-de-vos-contenus/"&gt;Ne vous laissez plus déPOSSEder de vos contenus !&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Did you see Heydon Pickering's “Why The IndieWeb?” episode of the Webbed Briefs?&lt;br&gt;&lt;br&gt;
&lt;a href="https://briefs.video/videos/why-the-indieweb/"&gt;You should!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every time I talk about IndieWeb and POSSE, a lot of people reply with “but it's not easy”… and they are right!&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://nicolas-hoizey.com/links/2022/11/15/the-indieweb-for-everyone/"&gt;Max Böck recently said&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So…&lt;/p&gt;

&lt;p&gt;I've developed &lt;strong&gt;a GitHub Action for anyone to POSSE their content to Mastodon as easily as possible&lt;/strong&gt; : &lt;a href="https://github.com/marketplace/actions/any-feed-to-mastodon"&gt;GitHub Action: Any feed to Mastoson&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It currently requires a JSON Feed for input, so you might still have to build this one, if you "only" have a RSS or Atom feed. I hope to &lt;a href="https://github.com/nhoizey/github-action-jsonfeed-to-mastodon/issues/16"&gt;support these also in the future&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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&lt;sup&gt;&lt;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"&gt;[1]&lt;/a&gt;&lt;/sup&gt;. 🤷‍♂️&lt;/p&gt;

&lt;p&gt;I won't paraphrase the Action's documentation, so go read it, use it, and tell me if it's useful:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/marketplace/actions/any-feed-to-mastodon"&gt;GitHub Action: Any feed to Mastoson&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have some ideas, bugs or anything to discuss about this action, &lt;a href="https://github.com/nhoizey/github-action-feed-to-mastodon/issues"&gt;GitHub issues&lt;/a&gt; are the right place.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;HTH.&lt;/p&gt;




&lt;ol&gt;
&lt;li id="fn1"&gt;
&lt;p&gt;and 99% of the Action is a Node script, so I can move anywhere else if necessary. &lt;a href="https://nicolas-hoizey.com/articles/2023/01/07/let-s-posse-to-mastodon-with-a-feed-and-a-github-action/#fnref1"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Accessible anchor links with Markdown-it and Eleventy</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Thu, 25 Feb 2021 21:08:34 +0000</pubDate>
      <link>https://forem.com/nhoizey/accessible-anchor-links-with-markdown-it-and-eleventy-4e7i</link>
      <guid>https://forem.com/nhoizey/accessible-anchor-links-with-markdown-it-and-eleventy-4e7i</guid>
      <description>&lt;p&gt;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 &lt;a href="https://web.dev/text-fragments/"&gt;Text Fragments&lt;/a&gt; might be a cross browser thing sometimes in the future. Here's how I made the anchor links of my &lt;a href="https://11ty.dev/"&gt;Eleventy&lt;/a&gt; based site accessible.&lt;/p&gt;

&lt;p&gt;I've been using the &lt;a href="https://github.com/valeriangalliat/markdown-it-anchor"&gt;markdown-it-anchors&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Were my Anchor Links Accessible?
&lt;/h2&gt;

&lt;p&gt;But a few weeks ago, I read this great detailed post where &lt;a href="https://amberwilson.co.uk/"&gt;Amber Wilson&lt;/a&gt; explains how she figured out how to make such anchor links really accessible: &lt;a href="https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/"&gt;Are your Anchor Links Accessible?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My anchor links were not accessible at all… 😱&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhancing &lt;code&gt;markdown-it-anchor&lt;/code&gt;'s rendering
&lt;/h2&gt;

&lt;p&gt;Amber also uses Eleventy and shared &lt;a href="https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/#automating-accessible-anchor-links"&gt;a new plugin to automate such accessible anchor links&lt;/a&gt;, but I wanted to keep the features I'm already using in &lt;code&gt;markdown-it-anchor&lt;/code&gt; and enhance it with better accessibility.&lt;/p&gt;

&lt;p&gt;Fortunately, &lt;code&gt;markdown-it-anchor&lt;/code&gt; provides &lt;a href="https://github.com/valeriangalliat/markdown-it-anchor#usage"&gt;a large set of options&lt;/a&gt;, including a way to provide our own rendering function with the &lt;code&gt;renderPermalink&lt;/code&gt; option. After a while diving into &lt;code&gt;markdown-it&lt;/code&gt; and &lt;code&gt;markdown-it-anchor&lt;/code&gt; 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! 🎉&lt;/p&gt;

&lt;p&gt;The code is primarily based on &lt;a href="https://github.com/valeriangalliat/markdown-it-anchor/blob/85afd1f054032d6a3c83102329c413b56cad99a9/index.js#L13-L34"&gt;&lt;code&gt;markdown-it-anchor&lt;/code&gt;'s default &lt;code&gt;renderPermalink&lt;/code&gt; function&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is &lt;a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/4c9e42b306a387e9533a1036a6286b7f24091ed4/.eleventy.js#L111-L176"&gt;my version&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;renderPermalink: (slug, opts, state, idx) =&amp;gt; {
  // based on fifth version in
  // https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
  const linkContent = state.tokens[idx + 1].children[0].content;

  // Create the openning &amp;lt;div&amp;gt; for the wrapper
  const headingWrapperTokenOpen = Object.assign(
    new state.Token('div_open', 'div', 1),
    {
      attrs: [['class', 'heading-wrapper']],
    }
  );
  // Create the closing &amp;lt;/div&amp;gt; for the wrapper
  const headingWrapperTokenClose = Object.assign(
    new state.Token('div_close', 'div', -1),
    {
      attrs: [['class', 'heading-wrapper']],
    }
  );

  // Create the tokens for the full accessible anchor link
  // &amp;lt;a class="deeplink" href="#your-own-platform-is-the-nearest-you-can-get-help-to-setup"&amp;gt;
  // &amp;lt;span aria-hidden="true"&amp;gt;
  // ${opts.permalinkSymbol}
  // &amp;lt;/span&amp;gt;
  // &amp;lt;span class="visually-hidden"&amp;gt;
  // Section titled Your "own" platform is the nearest you can(get help to) setup
  // &amp;lt;/span&amp;gt;
  // &amp;lt;/a &amp;gt;
  const anchorTokens = [
    Object.assign(new state.Token('link_open', 'a', 1), {
      attrs: [
        ...(opts.permalinkClass ? [['class', opts.permalinkClass]] : []),
        ['href', opts.permalinkHref(slug, state)],
        ...Object.entries(opts.permalinkAttrs(slug, state)),
      ],
    }),
    Object.assign(new state.Token('span_open', 'span', 1), {
      attrs: [['aria-hidden', 'true']],
    }),
    Object.assign(new state.Token('html_block', '', 0), {
      content: opts.permalinkSymbol,
    }),
    Object.assign(new state.Token('span_close', 'span', -1), {}),
    Object.assign(new state.Token('span_open', 'span', 1), {
      attrs: [['class', 'visually-hidden']],
    }),
    Object.assign(new state.Token('html_block', '', 0), {
      content: `Section titled ${linkContent}`,
    }),
    Object.assign(new state.Token('span_close', 'span', -1), {}),
    new state.Token('link_close', 'a', -1),
  ];

  // idx is the index of the heading's first token
  // insert the wrapper opening before the heading
  state.tokens.splice(idx, 0, headingWrapperTokenOpen);
  // insert the anchor link tokens after the wrapper opening and the 3 tokens of the heading
  state.tokens.splice(idx + 3 + 1, 0, ...anchorTokens);
  // insert the wrapper closing after all these
  state.tokens.splice(
    idx + 3 + 1 + anchorTokens.length,
    0,
    headingWrapperTokenClose
  );
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope there are enough comments in the code to understand how it works. The main &lt;code&gt;markdown-it&lt;/code&gt; behavior I had to understand is that &lt;a href="https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#token-stream"&gt;it uses an array of tokens to represent HTML nodes&lt;/a&gt;, instead of a more traditional &lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree"&gt;Abstract Syntax Tree&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting the CSS to the new HTML structure
&lt;/h2&gt;

&lt;p&gt;If you're already using &lt;code&gt;markdown-it-anchor&lt;/code&gt;, the anchor link is inside the heading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h3 id="were-my-anchor-links-accessible"&amp;gt;
  Were my Anchor Links Accessible?
  &amp;lt;a class="deeplink" href="#were-my-anchor-links-accessible"&amp;gt;#&amp;lt;/a&amp;gt;
&amp;lt;/h3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With my new code, following Amber advice, it is now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="heading-wrapper"&amp;gt;
  &amp;lt;h2 id="were-my-anchor-links-accessible"&amp;gt;Were my Anchor Links Accessible?&amp;lt;/h2&amp;gt;
  &amp;lt;a class="deeplink" href="#were-my-anchor-links-accessible"&amp;gt;
    &amp;lt;span aria-hidden="true"&amp;gt;#&amp;lt;/span&amp;gt;
    &amp;lt;span class="visually-hidden"&amp;gt;Section titled Were my Anchor Links Accessible?&amp;lt;/span&amp;gt;
  &amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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' &lt;a href="https://smolcss.dev/#smol-article-anchors"&gt;Smol Article Anchors&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Anchor links
// Based on https://smolcss.dev/#smol-article-anchors
.heading-wrapper {
  display: grid;
  // anchor link on the far right for long wrapping headings
  grid-template-columns: minmax(auto, max-content) min-content;
  align-items: stretch;
  gap: 0.5rem;
}

.deeplink {
  display: grid;
  justify-content: center;
  align-content: center;

  &amp;amp;:link,
  &amp;amp;:visited {
    padding: 0 0.25rem;
    border-radius: 0.3em;
    color: var(--color-meta);
    text-decoration: none;

    svg {
      fill: none;
      stroke: currentColor;
      stroke-width: 2px;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
  }

  .heading-wrapper:hover &amp;amp;,
  &amp;amp;:hover,
  &amp;amp;:focus {
    color: var(--color-link-hover);
    background-color: var(--color-link-hover-bg);
  }
}

@media (min-width: 65rem) {
  .heading-wrapper {
    // Anchor link in the left margin on larger viewports
    grid-template-columns: min-content auto;
    margin-left: -2rem; // 1rem width + .25rem * 2 paddings + 0.5rem gap
  }

  .deeplink {
    grid-row-start: 1;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On viewports &lt;code&gt;&amp;lt; 65rem&lt;/code&gt;, 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 &lt;code&gt;grid-template-columns: minmax(auto, max-content) min-content;&lt;/code&gt; is the best way to do it, feel free to suggest an enhancement.&lt;/p&gt;

&lt;p&gt;On viewports &lt;code&gt;&amp;gt;= 65rem&lt;/code&gt;, there is space around the content, so I move the anchor link in the margin on the left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhancing &lt;code&gt;markdown-it-anchor&lt;/code&gt; for everyone
&lt;/h2&gt;

&lt;p&gt;I asked &lt;a href="https://www.codejam.info/val.html"&gt;Valérian Galliat&lt;/a&gt;, maintainer of &lt;code&gt;markdown-it-anchor&lt;/code&gt;, if he would be open to merge a pull request providing this enhancement: &lt;a href="https://github.com/valeriangalliat/markdown-it-anchor/issues/82"&gt;https://github.com/valeriangalliat/markdown-it-anchor/issues/82&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Enhancing archives navigation, step 2</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Sun, 01 Nov 2020 22:55:51 +0000</pubDate>
      <link>https://forem.com/nhoizey/enhancing-archives-navigation-step-2-2fcd</link>
      <guid>https://forem.com/nhoizey/enhancing-archives-navigation-step-2-2fcd</guid>
      <description>&lt;p&gt;In my previous article &lt;a href="https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/"&gt;Enhancing archives navigation, step 1&lt;/a&gt;, I promised further archives navigation enhancements. Here they are!&lt;/p&gt;

&lt;p&gt;Remember how UX of navigation in archives by year and month where already enhanced with &lt;a href="https://github.com/nhoizey/nicolas-hoizey.com/blob/master/src/_layouts/archives.njk"&gt;a single Eleventy layout&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GbKjj1bl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/months-pagination-after.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GbKjj1bl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/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" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This new awesome layout made my build time go from 40 seconds to 300 seconds, a 650 % increase, not so awesome… 😅&lt;/p&gt;

&lt;p&gt;Now, imagine you want to see content from two — or more — types (&lt;a href="https://nicolas-hoizey.com/archives/?type=articles&amp;amp;type=notes"&gt;articles and notes&lt;/a&gt; for example), or mix not only one type and a date, but also the language, or tags, even multiple of them.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Time to &lt;a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/"&gt;enhance the already nice server-side rendering with awesome client-side features&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Here's what is now available for navigating the archives, &lt;a href="https://kryogenix.org/code/browser/everyonehasjs.html"&gt;if you activated JavaScript&lt;/a&gt; in your browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c6zYm--S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/articles/2020/11/02/enhancing-archives-navigation-step-2/archives-live-search-with-algolia.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c6zYm--S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/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" width="800" height="710"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a search input field, to search for any content, with live "as you type" results, and live updated filtering facets. 🤯&lt;/p&gt;

&lt;p&gt;If you search for something specific, the results highlight why they're here in the list:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---aZ6-W1H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/articles/2020/11/02/enhancing-archives-navigation-step-2/archives-live-search-with-algolia-highlight.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---aZ6-W1H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/articles/2020/11/02/enhancing-archives-navigation-step-2/archives-live-search-with-algolia-highlight.jpg" alt="Highlighted results" width="800" height="710"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RGIFXOYT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/assets/logos/algolia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RGIFXOYT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/assets/logos/algolia.png" alt="Algolia logo" width="800" height="275"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
All of this would not be possible without &lt;a href="https://nicolas-hoizey.com/tags/algolia/"&gt;Algolia&lt;/a&gt;, the awesome search service I've been using for multiple years.&lt;/p&gt;

&lt;p&gt;I inject all my contents in an Algolia index, and a single JavaScript script uses &lt;a href="https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/"&gt;Algolia's InstantSearch.js UI library&lt;/a&gt; to build the user interface and synchronize the search term and facets values to the URL (and back).&lt;/p&gt;

&lt;p&gt;What I really like here is that this is not the only way to browse the archives, it is "only" a (great) enhancement of what's available to anyone with the server-side rendering.&lt;/p&gt;

&lt;p&gt;I hope you'll enjoy this new feature!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Enhancing archives navigation, step 1</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Mon, 26 Oct 2020 14:16:23 +0000</pubDate>
      <link>https://forem.com/nhoizey/enhancing-archives-navigation-step-1-dal</link>
      <guid>https://forem.com/nhoizey/enhancing-archives-navigation-step-1-dal</guid>
      <description>&lt;p&gt;I decided years ago to remove paged navigation (aka "pagination"), 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.&lt;/p&gt;

&lt;p&gt;Here's how the month by month navigation was presented earlier, in the page bottom:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DmUBPI_u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/months-pagination-before.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DmUBPI_u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/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" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Instead of "just" 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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GbKjj1bl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/articles/2020/10/26/enhancing-archives-navigation-step-1/months-pagination-after.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GbKjj1bl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/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" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, you can navigate to the links I published in March 2019: &lt;a href="https://nicolas-hoizey.com/links/2019/03/"&gt;/links/2019/03/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I find it so easy to navigate, I wonder why I didn't have this idea earlier!&lt;/p&gt;

&lt;p&gt;I already had the required &lt;a href="https://github.com/nhoizey/nicolas-hoizey.com/tree/master/src/_11ty/collections"&gt;Eleventy collections&lt;/a&gt;, so almost everything was done in a single shared Nunjucks template. Not the easiest one, I recon.&lt;/p&gt;

&lt;p&gt;Now, I wonder if it's useful to keep the main navigation items for "&lt;a href="https://nicolas-hoizey.com/articles/"&gt;articles&lt;/a&gt;", "&lt;a href="https://nicolas-hoizey.com/links/"&gt;links&lt;/a&gt;", etc., or if the "&lt;a href="https://nicolas-hoizey.com/archives/"&gt;archives&lt;/a&gt;" navigation item is enough, which would obviously help on mobile.&lt;/p&gt;

&lt;p&gt;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!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How I build my SVG sprites</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Wed, 14 Oct 2020 22:02:19 +0000</pubDate>
      <link>https://forem.com/nhoizey/how-i-build-my-svg-sprites-2b2h</link>
      <guid>https://forem.com/nhoizey/how-i-build-my-svg-sprites-2b2h</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--n-_F_rX7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/assets/logos/feather-icons.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--n-_F_rX7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/assets/logos/feather-icons.png" alt="Feather icons" width="472" height="472"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
I'm using the very nice and open source &lt;a href="https://feathericons.com/"&gt;Feather icons&lt;/a&gt;. Feather provides &lt;a href="https://github.com/feathericons/feather#usage"&gt;multiples ways to use the icons&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;It's nice anyway that there's &lt;a href="https://www.npmjs.com/package/feather-icons"&gt;&lt;code&gt;feather-icons&lt;/code&gt; on npm&lt;/a&gt;, as adding it to my dev dependencies is enough to make sure I'll will always have the latest versions.&lt;/p&gt;

&lt;p&gt;Here's own these icons are used on this site:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iPevIuwY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://nicolas-hoizey.com/articles/2020/10/15/how-i-build-my-svg-sprites/feather-icons-in-metas.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iPevIuwY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/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" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;To build this sprite, I use &lt;a href="https://www.npmjs.com/package/svgstore"&gt;&lt;code&gt;svgstore&lt;/code&gt;&lt;/a&gt; in the following Node.js script, where comments should be enough to understand how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const svgstore = require('svgstore');
const fs = require('fs');
const path = require('path');

// Where are Feather icons available from the npm package?
const ICONS_FOLDER = 'node_modules/feather-icons/dist/icons/';

// Which icons do I need for the sprite?
// icon filename + title for accessibility
const ICONS_LIST = {
  calendar: 'Date',
  info: 'Info',
  link: 'Link',
  wifi: 'Online',
  'wifi-off': 'Offline',
  search: 'Search',
  tag: 'Tag',
  twitter: 'Twitter',
  'message-circle': 'Webmention',
};

// Initiate the sprite with svgstore
let sprite = svgstore({
  // Add these attributes to the sprite SVG
  svgAttrs: { style: 'display: none;', 'aria-hidden': 'true' },
  // Copy these attributes from the icon source SVG to the symbol in the sprite
  copyAttrs: ['width', 'height'],
});

// Loop through each icon in the list
Object.entries(ICONS_LIST).forEach(([icon, title]) =&amp;gt; {
  // Log the name of the icon and its title to the console
  console.log(`${icon}.svg -&amp;gt; ${title}`);
  const svgFile = fs
    // Load the content of the icon SVG file
    .readFileSync(path.join(ICONS_FOLDER, `${icon}.svg`), 'utf8')
    // Make its dimensions relative to the surounding text
    .replace(' width="24" height="24"', ' width="1em" height="1em"')
    // Remove useless attributes (for my usage) and add a title for accessibility
    .replace(
      / fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-[^"]+"&amp;gt;/,
      ` &amp;gt;&amp;lt;title id="${icon}-icon"&amp;gt;${title}&amp;lt;/title&amp;gt;`
    );
  // Add the new symbol to the sprite
  sprite.add(`symbol-${icon}`, svgFile, {
    // Add attributes for accessibility
    symbolAttrs: {
      'aria-labelledby': `${icon}-icon`,
      role: 'img',
    },
  });
});
// Finally, store the sprite in a file Eleventy will be able to include
fs.writeFileSync(
  'src/_includes/svg-sprite.svg',
  sprite.toString({ inline: true })
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is located in &lt;a href="https://github.com/nhoizey/nicolas-hoizey.com/tree/master/src/_utils"&gt;the &lt;code&gt;src/_utils/&lt;/code&gt; folder&lt;/a&gt; of my Eleventy project, with other scripts I run with &lt;code&gt;npm&lt;/code&gt;, like &lt;a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/#server-side-first"&gt;my Webmention update script&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;npm run svg&lt;/code&gt; when I feel it useful, and my build is a little faster.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>JAMstack is fast only if you make it so</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Wed, 07 Oct 2020 11:02:15 +0000</pubDate>
      <link>https://forem.com/nhoizey/jamstack-is-fast-only-if-you-make-it-so-2518</link>
      <guid>https://forem.com/nhoizey/jamstack-is-fast-only-if-you-make-it-so-2518</guid>
      <description>&lt;p&gt;JAMstack often promotes itself as an excellent way to provide performant sites. It's even the first listed benefit on &lt;a href="https://jamstack.wtf/"&gt;jamstack.wtf&lt;/a&gt;, a "guide [which] gathers the concept of JAMstack in a straight-forward guide to encourage other developers to adopt the workflow". But too many JAMstack sites are very slow.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Vous pouvez aussi lire la &lt;a href="https://jamstatic.fr/2020/10/05/la-jamstack-n-est-rapide-que-si-vous-la-rendez-rapide/"&gt;version française&lt;/a&gt;, merci &lt;a href="https://arnaudligny.fr/"&gt;Arnaud&lt;/a&gt; pour la traduction.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You may have seen &lt;a href="https://infrequently.org/"&gt;Alex Russell&lt;/a&gt;'s frequent rants about Gatsby:&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--nBQE76CF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/1293664489343074304/WvBkErp1_normal.png" alt="Alex Russell profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Alex Russell
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @slightlylate
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P4t6ys1m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      &lt;a href="https://twitter.com/matthewcp"&gt;@matthewcp&lt;/a&gt; &lt;a href="https://twitter.com/justinfagnani"&gt;@justinfagnani&lt;/a&gt; @gatsbyjs &lt;a href="https://twitter.com/kylemathews"&gt;@kylemathews&lt;/a&gt; &lt;a href="https://twitter.com/addyosmani"&gt;@addyosmani&lt;/a&gt; Looking across the full set of traces, modern Gatsby seems to produce pages that take 2-3x as long as they should to become interactive. &lt;br&gt;&lt;br&gt;This is not OK. Gatsby/NPM/React  regressively tax access to content.&lt;br&gt;&lt;br&gt;In less generous moments, I'd go as far as to say it's unethical.
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      22:30 PM - 17 Oct 2019
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1184959830819106816" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-reply-action.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1184959830819106816" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-retweet-action.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      3
      &lt;a href="https://twitter.com/intent/like?tweet_id=1184959830819106816" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-like-action.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
      17
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;
 

&lt;p&gt;Gatsby is an easy target (among many others) because it is currently not optimized for performance out of the box, despite what's &lt;a href="https://store.gatsbyjs.org/product/gatsby-sticker-6-pack"&gt;promoted&lt;/a&gt;. It is possible to fix it, for example with &lt;a href="https://www.gatsbyjs.org/packages/gatsby-plugin-no-javascript/"&gt;this plugin&lt;/a&gt;, and I believe good React developers can make it shine, but it should be the default, not an afterthought.&lt;/p&gt;

&lt;p&gt;Eleventy is very different, as Zach Leatherman reminds us in &lt;a href="https://www.zachleat.com/web/performance-dashboard/"&gt;Eleventy’s New Performance Leaderboard&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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 &lt;strong&gt;it also doesn’t add anything extra either&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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. &lt;a href="https://v8.dev/blog/cost-of-javascript-2019"&gt;It quickly impacts performance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="https://www.swyx.io/"&gt;swyx&lt;/a&gt; wrote &lt;a href="https://www.swyx.io/writing/clientside-webmentions/"&gt;Clientside Webmentions&lt;/a&gt; about implementing Webmention with &lt;a href="https://svelte.dev/"&gt;Svelte&lt;/a&gt;. Any article promoting &lt;a href="https://dev.to/tags/webmention/"&gt;Webmention&lt;/a&gt; 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.&lt;/p&gt;

&lt;h1&gt;
  
  
  Server-side first
&lt;/h1&gt;

&lt;p&gt;I prefer &lt;a href="https://nicolas-hoizey.com/articles/2017/07/27/so-long-disqus-hello-webmentions/#how-does-it-work-on-this-site"&gt;doing it on the server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It allows to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call webmentio.io API only when building the site, which should be less often than visitors viewing pages.&lt;/li&gt;
&lt;li&gt;cache the result of requests to &lt;a href="https://webmention.io"&gt;webmention.io&lt;/a&gt; and the timestamp of the latest, so that the next one only asks for new webmentions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It puts less pressure on webmention.io, with one single request per build, when a client implementation makes a much larger request (or even several, with pagination) for &lt;strong&gt;each&lt;/strong&gt; page view.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;my website received 75 webmentions in April 2020. I have probably built it a hundred times during the same period, so let's say &lt;strong&gt;100 requests to webmention.io with small responses&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;in the same period, my website had 3,746 page views (underestimated, I still use Google Analytics 🤷‍♂️), which would have made &lt;strong&gt;3,746 requests to webmention.io with large responses&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using the server-side build to get the webmentions provides multiple benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The performance for the users is much better, with HTML already computed on the server and statically served.&lt;/li&gt;
&lt;li&gt;Much fewer API calls, requiring much less computing time and power.&lt;/li&gt;
&lt;li&gt;Everyone should know that &lt;a href="https://aaronparecki.com/"&gt;Aaron Parecki&lt;/a&gt; provides the awesome webmention.io service &lt;strong&gt;for free&lt;/strong&gt;, and most Webmention users seem to use it nowadays, so being nice with its API feels better.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Enhance client-side, if really needed
&lt;/h1&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But remember every JavaScript added to the page has a cost, so the few additional webmentions have to be really useful.&lt;/p&gt;

&lt;p&gt;So, instead of doing this for every page view, at least:&lt;/p&gt;

&lt;p&gt;First, try to &lt;strong&gt;wait for some time after the site build&lt;/strong&gt; 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 "age" to query webmention.io less for older content that probably receives less webmentions, as &lt;a href="https://aarongustafson.github.io/jekyll-webmention_io/performance-tuning"&gt;Aaron Gustafson did even for server-side call in his Jekyll plugin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then, &lt;strong&gt;keep track of a user's calls to the API&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;h1&gt;
  
  
  Client-side only API calls sometimes make more sense
&lt;/h1&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webmentions to show are the same for all visitors.&lt;/li&gt;
&lt;li&gt;Missing a few of the latest ones is probably not an issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yes, many other use cases make client-side API calls necessary, or better than server-side ones, I understand that.&lt;/p&gt;

&lt;p&gt;I say &lt;strong&gt;it should not be the default&lt;/strong&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Promoting the &lt;del&gt;AJMstack&lt;/del&gt; Mstack
&lt;/h1&gt;

&lt;p&gt;That's also something I don't really like in current JAMstack trend, promoting &lt;strong&gt;J&lt;/strong&gt;avaScript and &lt;strong&gt;A&lt;/strong&gt;PIs much more than &lt;strong&gt;M&lt;/strong&gt;arkup.&lt;/p&gt;

&lt;p&gt;Here's for example what you can see on &lt;a href="https://jamstack.wtf/"&gt;jamstack.wtf&lt;/a&gt; (simplified):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Lr-OjUTV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/41ly8f1i6va083r773nf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Lr-OjUTV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/41ly8f1i6va083r773nf.png" alt="the horizontal JAMstack"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As suggested by &lt;a href="https://twitter.com/yann_yinn"&gt;Yann&lt;/a&gt;, I would like to start by using this better presentation&lt;sup id="fnref1"&gt;1&lt;/sup&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CqsaYOit--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2qkdkf399cz6yylnuogj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CqsaYOit--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2qkdkf399cz6yylnuogj.png" alt="the vertical JAMstack"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It makes more obvious there is a pile of things, quite useful for a "stack".&lt;/p&gt;

&lt;p&gt;But I would like to suggest this modification:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--K_Pw0Vvr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/zei2w9fivd6xe1sax2lh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--K_Pw0Vvr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/zei2w9fivd6xe1sax2lh.png" alt="the AJMstack"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, it reads as &lt;strong&gt;AJMstack&lt;/strong&gt; instead of JAMstack, so I bet I won't be successful promoting it… 🤷‍♂️&lt;/p&gt;

&lt;p&gt;But at least it feels more accurate, it shows JavaScript is the link between APIs and Markup.&lt;/p&gt;

&lt;p&gt;It even allows to present this as a great &lt;a href="https://dev.to/tags/progressive-enhancement/"&gt;progressive enhancement&lt;/a&gt; platform, as we can start with plain old (did I hear "boring"?) Markup…&lt;/p&gt;

&lt;p&gt;Here's the &lt;strong&gt;Mstack&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Gi7gQdT7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/qm1n1qxet3eh6zk92rbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Gi7gQdT7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/qm1n1qxet3eh6zk92rbi.png" alt="the Mstack"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sure this "stack" is great, and then enhance with JavaScript and APIs.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;CSS Grid and Flexbox are so fun to use, it took me just a few minutes to get this for the &lt;a href="https://nicolas-hoizey.com/articles/2020/05/05/jamstack-is-fast-only-if-you-make-it-so/"&gt;original post&lt;/a&gt;! 💪 ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>jamstack</category>
      <category>webperf</category>
      <category>gatsby</category>
      <category>eleventy</category>
    </item>
    <item>
      <title>Identify which Apache rewrite rules are used</title>
      <dc:creator>Nicolas Hoizey</dc:creator>
      <pubDate>Fri, 29 May 2020 10:13:01 +0000</pubDate>
      <link>https://forem.com/nhoizey/identify-which-apache-rewrite-rules-are-used-4ph5</link>
      <guid>https://forem.com/nhoizey/identify-which-apache-rewrite-rules-are-used-4ph5</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get some logs
&lt;/h2&gt;

&lt;p&gt;First, I had to tell &lt;a href="https://httpd.apache.org/docs/2.4/en/mod/mod_rewrite.html"&gt;Apache's &lt;code&gt;mod_rewrite&lt;/code&gt; module&lt;/a&gt;&lt;sup&gt;&lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn1" id="fnref1"&gt;[1]&lt;/a&gt;&lt;/sup&gt; to log more information than it usually does, but not too much either.&lt;/p&gt;

&lt;p&gt;Here's what I added to my Apache configuration&lt;sup&gt;&lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn2" id="fnref2"&gt;[2]&lt;/a&gt;&lt;/sup&gt; with the &lt;a href="https://httpd.apache.org/docs/2.4/en/mod/core.html#loglevel"&gt;LogLevel directive&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LogLevel warn rewrite:trace2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;warn&lt;/code&gt; is Apache's default log level, and &lt;code&gt;trace2&lt;/code&gt; is much more verbose, so I add it only for the &lt;code&gt;rewrite&lt;/code&gt; module.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter the logs for useful informations
&lt;/h2&gt;

&lt;p&gt;The logs I get with this are really verbose, and contain messages from Apache and all the active modules.&lt;/p&gt;

&lt;p&gt;Here is just a small extract for &lt;strong&gt;one single request&lt;/strong&gt; to &lt;code&gt;articles/2018/06/users-do-change-font-size/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is an old URL format where I didn't put the day as I do now, so there's a redirection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[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/' -&amp;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

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log file contains many lines like this, for all requests, on multiple domains sharing the same host. I had &lt;strong&gt;more than 4000 log lines per day&lt;/strong&gt; , even before adding the detailed logs for the &lt;code&gt;rewrite&lt;/code&gt; module, so it's impossible to navigate in these to get the information.&lt;/p&gt;

&lt;p&gt;Only the first of the eight lines above is useful to identify the redirection.&lt;/p&gt;

&lt;p&gt;So I chose to use simple shell tools&lt;sup&gt;&lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn3" id="fnref3"&gt;[3]&lt;/a&gt;&lt;/sup&gt; to filter the raw data and get only the useful lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat apache.log | grep 'nicolas-hoizey.com' | grep "] rewrite '"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; prints the content of the log file on the standard output, and then &lt;code&gt;grep&lt;/code&gt; filters lines containing three strings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nicolas-hoizey.com&lt;/code&gt; to get only logs for my domain&lt;sup&gt;&lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn4" id="fnref4"&gt;[4]&lt;/a&gt;&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;] rewrite '&lt;/code&gt; to make sure I get the rewrite instruction, not the others (&lt;code&gt;explicitly forcing redirect&lt;/code&gt;, &lt;code&gt;trying to replace prefix&lt;/code&gt;, &lt;code&gt;escaping&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So with that, I get all lines for redirections, great first step.&lt;/p&gt;

&lt;p&gt;But the useful data is "hidden" at the end of the line, after many things that can probably be useful sometimes, but not for my current use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract the useful value from the remaining logs
&lt;/h2&gt;

&lt;p&gt;Here is the full line broken into pieces (I won't explain it, just split the parts):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[Thu May 28 23:28:29.299495 2020]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[rewrite:trace2]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[pid 1241533:tid 140624745461504]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mod_rewrite.c(483):&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[client 92.169.204.166:0]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;92.169.204.166&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;- -&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[nicolas-hoizey.com/sid#7fe7132989d8]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[rid#7fe7104af0a0/initial]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[perdir /home/nhoizey/www/nicolas-hoizey.com/www/]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rewrite 'articles/2018/06/users-do-change-font-size/' -&amp;gt; 'https://nicolas-hoizey.com/articles/2018/06/15/users-do-change-font-size/'&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only useful part is the last one. I can remove everything before.&lt;/p&gt;

&lt;p&gt;I chose to use &lt;code&gt;sed&lt;/code&gt;&lt;sup&gt;&lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fn5" id="fnref5"&gt;[5]&lt;/a&gt;&lt;/sup&gt; to replace everything from the beginning of the line to the &lt;code&gt;] rewrite&lt;/code&gt; string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat apache.log | grep 'nicolas-hoizey.com' | grep "] rewrite '" | sed 's/^.*\] rewrite //'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then chose to get the list as a CSV file, so I also replaced the arrow in the middle (&lt;code&gt;-&amp;gt;&lt;/code&gt;) with a semi-colon, and I removed the domain from the second part to ease reading it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat apache.log | grep 'nicolas-hoizey.com' | grep "] rewrite '" | sed 's/^.*\] rewrite //' | sed 's/ -&amp;gt; /;/' | sed 's/https:\/\/nicolas-hoizey.com\///'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what I now get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'articles/2018/06/users-do-change-font-size/';'articles/2018/06/15/users-do-change-font-size/'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But with logs for multiple days, I get some identical redirections many times, so I chose to &lt;a href="https://unix.stackexchange.com/a/263849"&gt;sort them, keep only one of each, and count their number&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat apache.log | grep 'nicolas-hoizey.com' | grep "] rewrite '" | sed 's/^.*\] rewrite //' | sed 's/ -&amp;gt; /;/' | sed 's/https:\/\/nicolas-hoizey.com\///' | sort | uniq -c | sort -nr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, I chose to put all this in a file for later use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat apache.log | grep 'nicolas-hoizey.com' | grep "] rewrite '" | sed 's/^.*\] rewrite //' | sed 's/ -&amp;gt; /;/' | sed 's/https:\/\/nicolas-hoizey.com\///' | sort | uniq -c | sort -nr &amp;gt; ~/rewrites.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can now run this script, open the &lt;code&gt;.csv&lt;/code&gt; file in a spreadsheet, and see which of my redirections are still useful.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;839 different redirections&lt;/strong&gt; ), and I'll have to understand which ones are legitimate, and which others I can safely remove.&lt;/p&gt;

&lt;p&gt;For some of these redirections, the log also contains the referer, so I might be able to fix the URL at the source, like &lt;a href="https://indieweb.org/wiki/index.php?title=Webmention&amp;amp;type=revision&amp;amp;diff=70110&amp;amp;oldid=69713"&gt;I just did in the IndieWeb wiki&lt;/a&gt;.&lt;/p&gt;




&lt;ol&gt;
&lt;li id="fn1"&gt;
&lt;p&gt;I use &lt;code&gt;mod_rewrite&lt;/code&gt; for redirections because I need advanced URL manipulations &lt;a href="https://httpd.apache.org/docs/2.4/en/mod/mod_alias.html"&gt;&lt;code&gt;mod_alias&lt;/code&gt;&lt;/a&gt; doesn't allow. &lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref1"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn2"&gt;
&lt;p&gt;Unfortunately, this can not be set in an &lt;a href="https://httpd.apache.org/docs/2.4/en/howto/htaccess.html"&gt;&lt;code&gt;.htaccess&lt;/code&gt; file&lt;/a&gt; in your &lt;code&gt;DOCUMENT_ROOT&lt;/code&gt;, so you have to be able to change your Apache configuration, like my hosting &lt;a href="https://www.alwaysdata.com/en/"&gt;AlwaysData&lt;/a&gt; allows. &lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref2"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn3"&gt;
&lt;p&gt;I'm sure there are better tools, more powerful, but these ones allowed me to get the result I wanted. &lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref3"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn4"&gt;
&lt;p&gt;you might have a log file dedicated to one single domain, which would be easier to parse. &lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref4"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn5"&gt;
&lt;p&gt;I know &lt;code&gt;awk&lt;/code&gt; is more powerful, but I always forget the syntax, so &lt;code&gt;sed&lt;/code&gt; is fine. KISS. &lt;a href="https://nicolas-hoizey.com/articles/2020/05/29/identify-which-apache-rewrite-rules-are-used/#fnref5"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
  </channel>
</rss>
