<?xml version="1.0" encoding="utf-8"?><rss version="2.0"><channel><title>blog.responsive.ch</title><description>A blog about web technology and remotely related thingies.</description><link>https://blog.responsive.ch</link><item><title>Hopping off the (SSG) framework train</title><link>https://blog.responsive.ch/hopping-off-the-ssg-framework-train/</link><guid isPermaLink="true">https://blog.responsive.ch/hopping-off-the-ssg-framework-train/</guid><pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate><description><![CDATA[<p>Three years ago, this statically built blog <a href="/moving-to-astro">made the journey</a> from Gatsby to Astro. Astro&#39;s major version was 1 at the time. Now it&#39;s 5. Every major version of a framework comes with breaking changes. While they are mostly intentional, they might still require spending time understanding and implementing them.</p>
<p>So when I stumbled upon the following (non-public) post on Mastodon the other day, it hit a nerve:</p>
<blockquote>
<p>this week i&#39;ve lost several hours upgrading a site from Astro v4 to v5.
really looking forward to doing this again for v6 in a few months 🫠
[...]
i would like to rewrite my personal website without any sort of framework or static site generator.</p>
</blockquote>
<p>I was immediately nerd-sniped and knew what to do on a Sunday train ride to <a href="https://social.thomasjaggi.ch/@thomas/115435829060174716">sunny southern Switzerland</a>. What I ended up with is 400+ lines of JavaScript:</p>
<ul>
<li><a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/bin/build.js">build.js</a> transpiles Markdown files to HTML, creates a full-text RSS feed and bundles client-side scripts.</li>
<li><a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/bin/postprocess.js">postprocess.js</a> resizes images and transpiles and minifies styles.</li>
</ul>
<h2>How it works</h2>
<h3>HTML and RSS</h3>
<p>I chose <a href="https://marked.js.org">Marked</a> to compile posts written in Markdown to HTML because it comes with 0 dependencies and is well-documented. There are two plugins in use: <a href="https://www.npmjs.com/package/marked-shiki">marked-shiki</a> to add syntax highlighting to code blocks and <a href="https://www.npmjs.com/package/marked-footnote">marked-footnote</a>. Metadata like title, date and abstract is parsed using <a href="https://www.npmjs.com/package/front-matter">front-matter</a>.</p>
<p>The only outraging thing I stumbled upon so far: Marked seems to be unable to parse custom element names <a href="https://blog.jim-nielsen.com/2023/validity-of-custom-element-tag-names/">containing emojis</a>.</p>
<p>The <a href="/rss.xml">full-text RSS feed</a> is built with the help of <a href="https://www.npmjs.com/package/xml">xml</a> because I still don&#39;t understand CDATA. Fun fact: The full-text part was actually <a href="https://github.com/backflip/blog.responsive.ch/commit/b41153a3471c1b4a7d96c228f64458e286942b28">more complex</a> when still using Astro v1.</p>
<h3>Styles</h3>
<p>The single <a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/public/styles/main.css">CSS file</a> is copied as is and then postprocessed using <a href="https://lightningcss.dev">LightningCSS</a>. The processing consists of transpiling new syntax based on a <a href="https://browsersl.ist">Browserslist</a> config (flattening CSS nesting, rewriting color functions like <code>lch</code> etc.) and minification.</p>
<p>This is mostly optional: Should the postprocessing step make any issues at any point, I could just drop it and everything would still work fine (though styles would not be minified and some rules would be not be interpreted in older browsers).</p>
<h3>Responsive images</h3>
<p>The built HTML is parsed using <a href="https://www.npmjs.com/package/linkedom">LinkeDOM</a> to find all <code>img</code> elements with relative <code>src</code> URLs. Using <a href="https://sharp.pixelplumbing.com">Sharp</a>, images are transformed to WebP and resized based on a pre-defined list of dimensions.</p>
<p>Before:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">![</span><span style="color:#96D0FF">form route after POST request</span><span style="color:#ADBAC7">](</span><span style="color:#ADBAC7;text-decoration:underline">media/form-post.png</span><span style="color:#ADBAC7">)</span></span></code></pre>
<p>After:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">&#x3C;</span><span style="color:#8DDB8C">img</span></span>
<span class="line"><span style="color:#6CB6FF">  height</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"538"</span></span>
<span class="line"><span style="color:#6CB6FF">  width</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"800"</span></span>
<span class="line"><span style="color:#6CB6FF">  loading</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"lazy"</span></span>
<span class="line"><span style="color:#6CB6FF">  sizes</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"auto"</span></span>
<span class="line"><span style="color:#6CB6FF">  srcset</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"media/form-post-400.webp 400w, media/form-post-800.webp 800w"</span></span>
<span class="line"><span style="color:#6CB6FF">  src</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"media/form-post.png"</span></span>
<span class="line"><span style="color:#6CB6FF">  alt</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"form route after POST request"</span></span>
<span class="line"><span style="color:#ADBAC7">/></span></span></code></pre>
<p>So the postprocess script adds an <code>srcset</code> attribute with a few image sizes based on the original size of the image.</p>
<p>The magic part is the added <code>sizes=&quot;auto&quot;</code> as it frees me from figuring out specific sizes for specific images on specific viewports. Instead, the browser will pick the proper size. It has to be combined with <code>loading=&quot;lazy&quot;</code> so it would not be appropriate for keyvisuals, but works great for my content images.</p>
<p>This is totally awesome.</p>
<my-gifreplay>

<p><img src="./media/dancing.gif" alt="Nick Offerman as Ron Swanson dancing happily"></p>
</my-gifreplay>

<p>I really hope it will be part of <a href="https://github.com/web-platform-tests/interop/issues/1114">Interop 2026</a> as it&#39;s currently <a href="https://caniuse.com/mdn-html_elements_img_sizes_auto">Chromium-only</a>. Until then, the following step will bundle a <a href="https://github.com/Shopify/autosizes">polyfill</a> for other browser.</p>
<p>This processing step is optional, too. We&#39;ll still get working images should it ever fail, they are just larger than necessary. Same for non-Chromium browsers unable or unwilling to load and interpret the polyfill.</p>
<h3>Scripts</h3>
<p>The <a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/src/components/Layout.js#L24">layout component</a> references a single file:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">&#x3C;</span><span style="color:#8DDB8C">script</span><span style="color:#6CB6FF"> src</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"/scripts/index.js"</span><span style="color:#6CB6FF"> type</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"module"</span><span style="color:#ADBAC7">>&#x3C;/</span><span style="color:#8DDB8C">script</span><span style="color:#ADBAC7">></span></span></code></pre>
<p>It is created from a <a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/src/scripts/index.js">single entrypoint</a> using <a href="https://esbuild.github.io/">esbuild</a>. There are some hoops to jump through in order to map the Browserslist config to esbuild&#39;s <a href="https://www.npmjs.com/package/browserslist-to-esbuild">target option</a>, but nothing too fancy.</p>
<h2>Deployment</h2>
<p>A two-stage <a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/Dockerfile">Docker build</a> first runs the build and postprocess scripts and serves the result with <code>nginx</code>. A custom <a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/nginx.default.conf">config file</a> makes sure that we have a nice <a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/src/pages/404/index.md">404 page</a>.</p>
<p>This should work everywhere we can run a docker container. As a fan of <a href="https://fly.io">fly.io</a>, I have chosen their platform and deploy via their default <a href="https://github.com/backflip/blog.responsive.ch/blob/346d3efbddbfe5decaeb22613f9c70898c75b45c/.github/workflows/fly-deploy.yml">GitHub action</a>.</p>
<h2>Limitations</h2>
<p>Limitations?</p>
<my-gifreplay>

<p><img src="./media/how-dare-you.gif" alt="Nick Offerman as Lord Bruno in The Little Hours saying How Dare You!"></p>
</my-gifreplay>

<p>Okay, for one, there is no real development server. Instead, I serve the <code>dist/</code> folder using macOS&#39;s built-in <code>python3 -m http.server -d dist</code>. A simple <code>node --watch-path=./src ./bin/build.js</code> rebuilds everything on every <a href="https://nodejs.org/api/cli.html#--watch-path">change</a> of a markdown file and I manually reload the browser window whenever I feel like it. Which is not very often.</p>
<p>Second: Test coverage. Works on my machine.</p>
<p>Third: I don&#39;t yet understand the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag">ETag</a> caching behavior used by nginx (specifically, when my browser should decide to fetch updated styles or client-side scripts).</p>
<h2>Summary</h2>
<ul>
<li>Tired of regularly upgrading your SSG framework? There are two options:<ol>
<li>Replace the framework with another, fancier framework.</li>
<li>Build your own framework.</li>
</ol>
</li>
<li><code>sizes=&quot;auto&quot;</code> is the best.</li>
</ul>
]]></description></item><item><title>Running ImageMagick on older macOS</title><link>https://blog.responsive.ch/running-imagemagick-on-older-macos/</link><guid isPermaLink="true">https://blog.responsive.ch/running-imagemagick-on-older-macos/</guid><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><description><![CDATA[<p>We reanimated an old MacBook for the kid so he could download pictures of planes, trains and automobiles, and create his own memory sets (a card game which is apparently called <a href="https://en.wikipedia.org/wiki/Concentration_(card_game)">concentration</a> in non-German speaking countries). Turns out the latest macOS running on that particular hardware (<a href="https://en.wikipedia.org/wiki/MacOS_Catalina">10.15 Catalina</a>) does not yet support WebP or AVIF images outside of a browser. No problem, let&#39;s <a href="https://formulae.brew.sh/formula/imagemagick">brew ourselves</a> some <a href="https://imagemagick.org">ImageMagick</a> so we can convert them to PNGs.</p>
<p>Nope. Turns out a dependency (an ingredient?) of that formula, a recent OpenSSL version, <a href="https://github.com/openssl/openssl/pull/27941">assumes as specific Perl dependency</a> is always present. Not on this machine. As some people on the internet recommended against manipulating the system version of Perl, I went with a new homebrewed one and installed the missing <a href="https://metacpan.org/pod/IO::Socket::IP">IO::Socket::IP</a> via <a href="https://www.cpan.org/modules/INSTALL.html"><code>cpan</code></a>. Still no luck, as installing OpenSSL apparently ignored my new perl version. At some point this was working somehow, but then I stumbled upon the next failing dependency.</p>
<p>I was expecting something similar to <a href="https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides">npm&#39;s overrides</a> to exist, allowing me to pin an old version of any incompatible dependeny. But based on what I gathered from <a href="https://github.com/Homebrew/brew/issues/6103">discussions in Homebrew&#39;s issue tracker</a>, that is not something they want to provide. Which is very unfortunate for my case but totally understandable from an open-source software support point-of-view.</p>
<p>So I gave up and turned to Docker:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>FROM debian:latest</span></span>
<span class="line"><span>RUN apt-get update &#x26;&#x26; apt-get install -y imagemagick</span></span>
<span class="line"><span>ENTRYPOINT ["magick"]</span></span></code></pre>
<p>After building this image with <code>docker build . -t imagemagick</code>, it can be used to convert all <code>.webp</code> and <code>.avif</code> files in the current directory (recursively) to PNG:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F47067">for</span><span style="color:#ADBAC7"> f </span><span style="color:#F47067">in</span><span style="color:#ADBAC7"> $(</span><span style="color:#F69D50">find</span><span style="color:#96D0FF"> .</span><span style="color:#6CB6FF"> -name</span><span style="color:#96D0FF"> '*.webp'</span><span style="color:#6CB6FF"> -o</span><span style="color:#6CB6FF"> -name</span><span style="color:#96D0FF"> '*.avif'</span><span style="color:#ADBAC7">); </span><span style="color:#F47067">do</span></span>
<span class="line"><span style="color:#F69D50">  docker</span><span style="color:#96D0FF"> run</span><span style="color:#6CB6FF"> -v</span><span style="color:#ADBAC7"> $(</span><span style="color:#6CB6FF">pwd</span><span style="color:#ADBAC7">)</span><span style="color:#96D0FF">:/i</span><span style="color:#96D0FF"> imagemagick</span><span style="color:#96D0FF"> /i/</span><span style="color:#ADBAC7">$f </span><span style="color:#96D0FF">/i/</span><span style="color:#ADBAC7">$f</span><span style="color:#96D0FF">.png</span></span>
<span class="line"><span style="color:#F47067">done</span></span></code></pre>
<p>However, this image has a ridiculous size of 380 MB. So let&#39;s pick a smaller base image:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>FROM alpine:latest</span></span>
<span class="line"><span>RUN apk add --no-cache imagemagick</span></span>
<span class="line"><span>ENTRYPOINT ["magick"]</span></span></code></pre>
<p>While this reduces the size to 33 MB, it has the slight disadvantage of not working:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>magick: no decode delegate for this image format `AVIF' @ error/constitute.c/ReadImage/746.</span></span>
<span class="line"><span>magick: delegate failed `'dwebp' -pam '%i' -o '%o'' @ error/delegate.c/InvokeDelegate/1920.</span></span>
<span class="line"><span>magick: unable to open file '/tmp/magick-WH2QY7r79RAmEmO-qFYO0Lv219JnOAbk': No such file or directory @ error/constitute.c/ReadImage/785.</span></span></code></pre>
<p>So let&#39;s add the missing packages:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>FROM alpine:latest</span></span>
<span class="line"><span>RUN apk add --no-cache imagemagick libwebp libheif</span></span>
<span class="line"><span>ENTRYPOINT ["magick"]</span></span></code></pre>
<p>This increases the built image&#39;s size to 51 MB (~0.5 MB for <code>libwebp</code> and the rest for <code>libheif</code>), but we are still almost 90% below the initial size. 🎉</p>
<p>So as long as Docker runs on macOS Catalina, we are probably safe.</p>
]]></description></item><item><title>SVG Sprites for HTML and CSS</title><link>https://blog.responsive.ch/svg-sprites-for-html-and-css/</link><guid isPermaLink="true">https://blog.responsive.ch/svg-sprites-for-html-and-css/</guid><pubDate>Tue, 30 Jul 2024 00:00:00 GMT</pubDate><description><![CDATA[<p>SVG sprites are a great way to serve icons. They have been around for <a href="https://css-tricks.com/svg-sprites-use-better-icon-fonts/">quite some time</a>.</p>
<p>The basic approach:</p>
<ol>
<li>Add each icon as a <code>&lt;symbol&gt;</code> to a single <code>sprite-symbols.svg</code> file:<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">&#x3C;</span><span style="color:#8DDB8C">svg</span><span style="color:#6CB6FF"> xmlns</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"http://www.w3.org/2000/svg"</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">symbol</span><span style="color:#6CB6FF"> viewBox</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0 -960 960 960"</span><span style="color:#6CB6FF"> id</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"search"</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">    &#x3C;</span><span style="color:#8DDB8C">path</span></span>
<span class="line"><span style="color:#6CB6FF">      d</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"</span></span>
<span class="line"><span style="color:#ADBAC7">    /></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;/</span><span style="color:#8DDB8C">symbol</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">symbol</span><span style="color:#6CB6FF"> viewBox</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0 -960 960 960"</span><span style="color:#6CB6FF"> id</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"close"</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">    &#x3C;</span><span style="color:#8DDB8C">path</span></span>
<span class="line"><span style="color:#6CB6FF">      d</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"</span></span>
<span class="line"><span style="color:#ADBAC7">    /></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;/</span><span style="color:#8DDB8C">symbol</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">&#x3C;/</span><span style="color:#8DDB8C">svg</span><span style="color:#ADBAC7">></span></span></code></pre>
</li>
<li>Render SVG with <code>use</code> element. Reference specific symbol via fragment identifier:<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">&#x3C;</span><span style="color:#8DDB8C">svg</span><span style="color:#6CB6FF"> class</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"icon"</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">use</span><span style="color:#6CB6FF"> xlink:href</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"sprite-symbols.svg#search"</span><span style="color:#ADBAC7"> /></span></span>
<span class="line"><span style="color:#ADBAC7">&#x3C;/</span><span style="color:#8DDB8C">svg</span><span style="color:#ADBAC7">></span></span></code></pre>
</li>
<li>Set color via CSS:<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#6CB6FF">.icon</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  fill</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">currentColor</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
</li>
</ol>
<p>The result:</p>
<style>
.icon-button {
  appearance: none;
  color: black;
  background: white;
  border: 1px solid;
  border-radius: 5px;
	display: inline-flex;
	align-items: center;
}
.icon-button svg {
	fill: currentColor;
	width: 2rem;
	aspect-ratio: 1;
}
.icon-button:hover,
.icon-button:focus {
	color: red;
}
</style>

<button type="button" class="icon-button">
  <svg>
    <use xlink:href="media/sprite-symbols.svg#search" />
  </svg>
  <span>Search</span>
</button>
<button type="button" class="icon-button">
  <svg>
    <use xlink:href="media/sprite-symbols.svg#close" />
  </svg>
  <span>Close</span>
</button>

<h2>How to reference the sprite in CSS?</h2>
<p>What if we are unable to control the HTML and want to render the icon via CSS instead?</p>
<p>Let&#39;s add a background image:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">&#x3C;</span><span style="color:#8DDB8C">style</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#6CB6FF">  .var-background::before</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">    background-image</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">url</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">sprite-symbols.svg#search</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#768390">    /* ... */</span></span>
<span class="line"><span style="color:#ADBAC7">  }</span></span>
<span class="line"><span style="color:#ADBAC7">&#x3C;/</span><span style="color:#8DDB8C">style</span><span style="color:#ADBAC7">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">&#x3C;</span><span style="color:#8DDB8C">button</span><span style="color:#6CB6FF"> type</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"button"</span><span style="color:#6CB6FF"> class</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"icon-button var-background"</span><span style="color:#ADBAC7">>Search&#x3C;/</span><span style="color:#8DDB8C">button</span><span style="color:#ADBAC7">></span></span></code></pre>
<p>Unfortunately, the icon is not rendered:</p>
<style>
.var-background::before {
	background-image: url(media/sprite-symbols.svg#search);
	content: "";
	width: 2rem;
	aspect-ratio: 1;
}
</style>

<button type="button" class="icon-button var-background">
  <span>Search</span>
</button>

<p>For this to work, we need to expose a <a href="https://css-tricks.com/svg-fragment-identifiers-work/"><code>&lt;view&gt;</code></a> element in the sprite. Fortunately, it can be combined with our existing <code>&lt;symbol&gt;</code> via a <code>&lt;use&gt;</code> intermediate:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">&#x3C;</span><span style="color:#8DDB8C">svg</span></span>
<span class="line"><span style="color:#6CB6FF">  xmlns</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"http://www.w3.org/2000/svg"</span></span>
<span class="line"><span style="color:#6CB6FF">  xmlns:xlink</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"http://www.w3.org/1999/xlink"</span></span>
<span class="line"><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">symbol</span><span style="color:#6CB6FF"> viewBox</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0 -960 960 960"</span><span style="color:#6CB6FF"> id</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"search"</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">    &#x3C;</span><span style="color:#8DDB8C">path</span></span>
<span class="line"><span style="color:#6CB6FF">      d</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"</span></span>
<span class="line"><span style="color:#ADBAC7">    /></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;/</span><span style="color:#8DDB8C">symbol</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">use</span><span style="color:#6CB6FF"> xlink:href</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"#search"</span><span style="color:#6CB6FF"> width</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"960"</span><span style="color:#6CB6FF"> height</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"960"</span><span style="color:#6CB6FF"> x</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0"</span><span style="color:#6CB6FF"> y</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0"</span><span style="color:#ADBAC7">>&#x3C;/</span><span style="color:#8DDB8C">use</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">view</span><span style="color:#6CB6FF"> id</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"search-view"</span><span style="color:#6CB6FF"> viewBox</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0 0 960 960"</span><span style="color:#ADBAC7"> /></span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">symbol</span><span style="color:#6CB6FF"> viewBox</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0 -960 960 960"</span><span style="color:#6CB6FF"> id</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"close"</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">    &#x3C;</span><span style="color:#8DDB8C">path</span></span>
<span class="line"><span style="color:#6CB6FF">      d</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"</span></span>
<span class="line"><span style="color:#ADBAC7">    /></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;/</span><span style="color:#8DDB8C">symbol</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">use</span><span style="color:#6CB6FF"> xlink:href</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"#close"</span><span style="color:#6CB6FF"> width</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"960"</span><span style="color:#6CB6FF"> height</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"960"</span><span style="color:#6CB6FF"> x</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0"</span><span style="color:#6CB6FF"> y</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"960"</span><span style="color:#ADBAC7">>&#x3C;/</span><span style="color:#8DDB8C">use</span><span style="color:#ADBAC7">></span></span>
<span class="line"><span style="color:#ADBAC7">  &#x3C;</span><span style="color:#8DDB8C">view</span><span style="color:#6CB6FF"> id</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"close-view"</span><span style="color:#6CB6FF"> viewBox</span><span style="color:#ADBAC7">=</span><span style="color:#96D0FF">"0 960 960 960"</span><span style="color:#ADBAC7"> /></span></span>
<span class="line"><span style="color:#ADBAC7">&#x3C;/</span><span style="color:#8DDB8C">svg</span><span style="color:#ADBAC7">></span></span></code></pre>
<p>Now we can use the fragment identifier of the new <code>&lt;view id=&quot;search-view&quot; /&gt;</code> element:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#6CB6FF">.var-view::before</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background-image</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">url</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">sprite-hybrid.svg#search-view</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#768390">  /* ... */</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
<p>The updated result:</p>
<style>
.var-view::before {
	background-image: url(media/sprite-hybrid.svg#search-view);
}
</style>

<button type="button" class="icon-button var-background var-view">
  <span>Search</span>
</button>

<h2>How to change color on hover/active?</h2>
<p><code>mask-image</code> to the rescue. Instead of using <code>background-image</code>, we set a background color and use the icon as a mask:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#6CB6FF">.var-mask::before</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">currentColor</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  mask-image</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">url</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">sprite-hybrid.svg#search-view</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#768390">  /** ... */</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
<p>The final result:</p>
<style>
.var-mask::before {
	background: currentColor;
	mask-image: url(media/sprite-hybrid.svg#search-view);
}
</style>

<button type="button" class="icon-button var-background var-view var-mask">
  <span>Search</span>
</button>

<h2>How to automate this?</h2>
<p>I&#39;m currently using the highly recommended <a href="https://github.com/svg-sprite/svg-sprite">svg-sprite</a> library to generate SVG sprites from a folder of icons. At the time of writing this article, it does not yet provide a <a href="https://github.com/svg-sprite/svg-sprite/issues/678">hybrid mode</a>. However, since its <a href="https://github.com/svg-sprite/svg-sprite/blob/main/docs/api.md">compile method</a> returns both the generated SVG and all shapes, we can use the <code>symbol</code> <a href="https://github.com/svg-sprite/svg-sprite/blob/main/docs/configuration.md#output-modes">output mode</a> and manually extend the generated SVG with a <code>&lt;use ... /&gt;&lt;view ... /&gt;</code> for each symbol by <a href="https://github.com/svg-sprite/svg-sprite/issues/678#issuecomment-2257095890">looping through the shapes</a>.</p>
]]></description></item><item><title>Minimal Node.js server</title><link>https://blog.responsive.ch/minimal-node-server/</link><guid isPermaLink="true">https://blog.responsive.ch/minimal-node-server/</guid><pubDate>Wed, 11 Oct 2023 00:00:00 GMT</pubDate><description><![CDATA[<p>I absolutely love building small web applications.</p>
<p>We have to scroll too long to find our son&#39;s favorite kind of bedtime stories on <a href="https://www.srf.ch/play/tv/sendung/guetnachtgschichtli?id=c522b312-1f70-0001-f1f0-817c12301734">srf.ch</a>? Let&#39;s extract them and create our own listing where we can keep track of what we have watched, too.</p>
<p>I&#39;m annoyed with the local library&#39;s (single-page) application because it takes 25 clicks to see the books we have borrowed? Let&#39;s write a Puppeteer script to export them on an a regular basis and list them on a website along with the current opening hours.</p>
<p>My basic requirements for these mini-projects:</p>
<ul>
<li>Routing</li>
<li>POST request body parsing</li>
<li>Watch mode during development</li>
</ul>
<p>In the old days, I would usually opt for something like <a href="https://nextjs.org">Next.js</a>. And whenever I wanted to change something later, I had to spend much more time maintaining dependencies than actually writing code.</p>
<p>My new setup:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#96D0FF">"dependencies"</span><span style="color:#ADBAC7">: {},</span></span>
<span class="line"><span style="color:#96D0FF">"devDependencies"</span><span style="color:#ADBAC7">: {}</span></span></code></pre>
<h2>What it does</h2>
<p>The demo app has two routes, one of them handling POST requests (assuming the <code>x-www-form-urlencoded</code> content type of a basic <code>&lt;form method=&quot;POST&quot;&gt;</code>):</p>
<ul>
<li><code>GET /</code>: <img src="media/index.png" alt="index route"></li>
<li><code>GET /form</code>: <img src="media/form.png" alt="form route"></li>
<li><code>POST /form</code>: <img src="media/form-post.png" alt="form route after POST request"></li>
</ul>
<h2>What we need</h2>
<p>This is the complete code:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#768390">// @ts-check</span></span>
<span class="line"><span style="color:#F47067">import</span><span style="color:#ADBAC7"> http </span><span style="color:#F47067">from</span><span style="color:#96D0FF"> "node:http"</span><span style="color:#ADBAC7">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">const</span><span style="color:#6CB6FF"> port</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> process.env.</span><span style="color:#6CB6FF">PORT</span><span style="color:#F47067"> ||</span><span style="color:#6CB6FF"> 3000</span><span style="color:#ADBAC7">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/**</span></span>
<span class="line"><span style="color:#768390"> * Parse POST request body</span></span>
<span class="line"><span style="color:#768390"> * </span><span style="color:#F47067">@param</span><span style="color:#F69D50"> {import('http').IncomingMessage}</span><span style="color:#ADBAC7"> req</span></span>
<span class="line"><span style="color:#768390"> * </span><span style="color:#F47067">@returns</span><span style="color:#F69D50"> {Promise&#x3C;{ [key: string]: string | string[] }>}</span></span>
<span class="line"><span style="color:#768390"> */</span></span>
<span class="line"><span style="color:#F47067">async</span><span style="color:#F47067"> function</span><span style="color:#DCBDFB"> parseBody</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">req</span><span style="color:#ADBAC7">) {</span></span>
<span class="line"><span style="color:#F47067">  const</span><span style="color:#6CB6FF"> body</span><span style="color:#F47067"> =</span><span style="color:#F47067"> await</span><span style="color:#F47067"> new</span><span style="color:#6CB6FF"> Promise</span><span style="color:#ADBAC7">((</span><span style="color:#F69D50">resolve</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#F47067">    let</span><span style="color:#ADBAC7"> data </span><span style="color:#F47067">=</span><span style="color:#ADBAC7"> [];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">    req</span></span>
<span class="line"><span style="color:#ADBAC7">      .</span><span style="color:#DCBDFB">on</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"data"</span><span style="color:#ADBAC7">, (</span><span style="color:#F69D50">chunk</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#ADBAC7">        data.</span><span style="color:#DCBDFB">push</span><span style="color:#ADBAC7">(chunk);</span></span>
<span class="line"><span style="color:#ADBAC7">      })</span></span>
<span class="line"><span style="color:#ADBAC7">      .</span><span style="color:#DCBDFB">on</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"end"</span><span style="color:#ADBAC7">, () </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#F47067">        const</span><span style="color:#6CB6FF"> str</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> Buffer.</span><span style="color:#DCBDFB">concat</span><span style="color:#ADBAC7">(data).</span><span style="color:#DCBDFB">toString</span><span style="color:#ADBAC7">();</span></span>
<span class="line"><span style="color:#F47067">        const</span><span style="color:#6CB6FF"> searchParams</span><span style="color:#F47067"> =</span><span style="color:#F47067"> new</span><span style="color:#DCBDFB"> URLSearchParams</span><span style="color:#ADBAC7">(str);</span></span>
<span class="line"><span style="color:#F47067">        const</span><span style="color:#6CB6FF"> obj</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> {};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">        for</span><span style="color:#ADBAC7"> (</span><span style="color:#F47067">const</span><span style="color:#6CB6FF"> key</span><span style="color:#F47067"> of</span><span style="color:#ADBAC7"> searchParams.</span><span style="color:#DCBDFB">keys</span><span style="color:#ADBAC7">()) {</span></span>
<span class="line"><span style="color:#F47067">          const</span><span style="color:#6CB6FF"> values</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> searchParams.</span><span style="color:#DCBDFB">getAll</span><span style="color:#ADBAC7">(key);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">          obj[key] </span><span style="color:#F47067">=</span><span style="color:#ADBAC7"> values.</span><span style="color:#6CB6FF">length</span><span style="color:#F47067"> ></span><span style="color:#6CB6FF"> 1</span><span style="color:#F47067"> ?</span><span style="color:#ADBAC7"> values </span><span style="color:#F47067">:</span><span style="color:#ADBAC7"> values[</span><span style="color:#6CB6FF">0</span><span style="color:#ADBAC7">];</span></span>
<span class="line"><span style="color:#ADBAC7">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">        return</span><span style="color:#DCBDFB"> resolve</span><span style="color:#ADBAC7">(obj);</span></span>
<span class="line"><span style="color:#ADBAC7">      });</span></span>
<span class="line"><span style="color:#ADBAC7">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">  return</span><span style="color:#ADBAC7"> body;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/**</span></span>
<span class="line"><span style="color:#768390"> * Render HTML document</span></span>
<span class="line"><span style="color:#768390"> * </span><span style="color:#F47067">@param</span><span style="color:#F69D50"> {{ title?: string, content?: string }}</span><span style="color:#ADBAC7"> props</span></span>
<span class="line"><span style="color:#768390"> * </span><span style="color:#F47067">@returns</span><span style="color:#F69D50"> {string}</span></span>
<span class="line"><span style="color:#768390"> */</span></span>
<span class="line"><span style="color:#F47067">function</span><span style="color:#DCBDFB"> renderHtml</span><span style="color:#ADBAC7">({ </span><span style="color:#F69D50">title</span><span style="color:#F47067"> =</span><span style="color:#96D0FF"> "Title"</span><span style="color:#ADBAC7">, </span><span style="color:#F69D50">content</span><span style="color:#F47067"> =</span><span style="color:#96D0FF"> "Content"</span><span style="color:#ADBAC7"> } </span><span style="color:#F47067">=</span><span style="color:#ADBAC7"> {}) {</span></span>
<span class="line"><span style="color:#F47067">  return</span><span style="color:#96D0FF"> `&#x3C;!DOCTYPE html></span></span>
<span class="line"><span style="color:#96D0FF">		&#x3C;html lang="en"></span></span>
<span class="line"><span style="color:#96D0FF">			&#x3C;head></span></span>
<span class="line"><span style="color:#96D0FF">				&#x3C;meta charset="utf-8"></span></span>
<span class="line"><span style="color:#96D0FF">				&#x3C;title>${</span><span style="color:#ADBAC7">title</span><span style="color:#96D0FF">}&#x3C;/title></span></span>
<span class="line"><span style="color:#96D0FF">				&#x3C;style>body { font-family: sans-serif; }&#x3C;/style></span></span>
<span class="line"><span style="color:#96D0FF">				&#x3C;link rel="icon" href="data:image/svg+xml,&#x3C;svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22>&#x3C;text y=%22.9em%22 font-size=%2290%22>👋&#x3C;/text>&#x3C;/svg>"></span></span>
<span class="line"><span style="color:#96D0FF">			&#x3C;/head></span></span>
<span class="line"><span style="color:#96D0FF">			&#x3C;body></span></span>
<span class="line"><span style="color:#96D0FF">				&#x3C;h1>${</span><span style="color:#ADBAC7">title</span><span style="color:#96D0FF">}&#x3C;/h1></span></span>
<span class="line"><span style="color:#96D0FF">				${</span><span style="color:#ADBAC7">content</span><span style="color:#96D0FF">}</span></span>
<span class="line"><span style="color:#96D0FF">			&#x3C;/body></span></span>
<span class="line"><span style="color:#96D0FF">		&#x3C;/html></span></span>
<span class="line"><span style="color:#96D0FF">	`</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/**</span></span>
<span class="line"><span style="color:#768390"> * Start server on `port`</span></span>
<span class="line"><span style="color:#768390"> */</span></span>
<span class="line"><span style="color:#ADBAC7">http</span></span>
<span class="line"><span style="color:#ADBAC7">  .</span><span style="color:#DCBDFB">createServer</span><span style="color:#ADBAC7">(</span><span style="color:#F47067">async</span><span style="color:#ADBAC7"> (</span><span style="color:#F69D50">req</span><span style="color:#ADBAC7">, </span><span style="color:#F69D50">res</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#F47067">    const</span><span style="color:#6CB6FF"> requestUrl</span><span style="color:#F47067"> =</span><span style="color:#F47067"> new</span><span style="color:#DCBDFB"> URL</span><span style="color:#ADBAC7">(req.url </span><span style="color:#F47067">||</span><span style="color:#96D0FF"> ""</span><span style="color:#ADBAC7">, </span><span style="color:#96D0FF">`http://${</span><span style="color:#ADBAC7">req</span><span style="color:#96D0FF">.</span><span style="color:#ADBAC7">headers</span><span style="color:#96D0FF">.</span><span style="color:#ADBAC7">host</span><span style="color:#96D0FF">}`</span><span style="color:#ADBAC7">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">    // Route: `/`</span></span>
<span class="line"><span style="color:#F47067">    if</span><span style="color:#ADBAC7"> (requestUrl.pathname </span><span style="color:#F47067">===</span><span style="color:#96D0FF"> "/"</span><span style="color:#ADBAC7">) {</span></span>
<span class="line"><span style="color:#F47067">      const</span><span style="color:#6CB6FF"> html</span><span style="color:#F47067"> =</span><span style="color:#DCBDFB"> renderHtml</span><span style="color:#ADBAC7">({</span></span>
<span class="line"><span style="color:#ADBAC7">        content: </span><span style="color:#96D0FF">`&#x3C;p>&#x3C;a href="/form">Form example&#x3C;/a>&#x3C;/p>`</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#ADBAC7">      });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">      res.</span><span style="color:#DCBDFB">writeHead</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">200</span><span style="color:#ADBAC7">, { </span><span style="color:#96D0FF">"Content-Type"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"text/html"</span><span style="color:#ADBAC7"> });</span></span>
<span class="line"><span style="color:#ADBAC7">      res.</span><span style="color:#DCBDFB">end</span><span style="color:#ADBAC7">(html);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">      return</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">    // Route: `/form`</span></span>
<span class="line"><span style="color:#F47067">    if</span><span style="color:#ADBAC7"> (requestUrl.pathname </span><span style="color:#F47067">===</span><span style="color:#96D0FF"> "/form"</span><span style="color:#ADBAC7">) {</span></span>
<span class="line"><span style="color:#F47067">      let</span><span style="color:#ADBAC7"> message </span><span style="color:#F47067">=</span><span style="color:#96D0FF"> ""</span><span style="color:#ADBAC7">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">      if</span><span style="color:#ADBAC7"> (req.method </span><span style="color:#F47067">===</span><span style="color:#96D0FF"> "POST"</span><span style="color:#ADBAC7">) {</span></span>
<span class="line"><span style="color:#F47067">        const</span><span style="color:#6CB6FF"> body</span><span style="color:#F47067"> =</span><span style="color:#F47067"> await</span><span style="color:#DCBDFB"> parseBody</span><span style="color:#ADBAC7">(req);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">        message </span><span style="color:#F47067">=</span><span style="color:#96D0FF"> `&#x3C;pre>body: ${</span><span style="color:#6CB6FF">JSON</span><span style="color:#96D0FF">.</span><span style="color:#DCBDFB">stringify</span><span style="color:#96D0FF">(</span><span style="color:#ADBAC7">body</span><span style="color:#96D0FF">, </span><span style="color:#6CB6FF">null</span><span style="color:#96D0FF">, </span><span style="color:#96D0FF">"  "</span><span style="color:#96D0FF">)</span><span style="color:#96D0FF">}&#x3C;/pre>`</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">      }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">      const</span><span style="color:#6CB6FF"> html</span><span style="color:#F47067"> =</span><span style="color:#DCBDFB"> renderHtml</span><span style="color:#ADBAC7">({</span></span>
<span class="line"><span style="color:#ADBAC7">        title: </span><span style="color:#96D0FF">"Form"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#ADBAC7">        content: </span><span style="color:#96D0FF">`${</span><span style="color:#ADBAC7">message</span><span style="color:#96D0FF">}</span></span>
<span class="line"><span style="color:#96D0FF">					&#x3C;form action="" method="POST"></span></span>
<span class="line"><span style="color:#96D0FF">						&#x3C;p></span></span>
<span class="line"><span style="color:#96D0FF">							&#x3C;label for="name">Name&#x3C;/label></span></span>
<span class="line"><span style="color:#96D0FF">							&#x3C;input type="text" name="name" id="name"></span></span>
<span class="line"><span style="color:#96D0FF">						&#x3C;/p></span></span>
<span class="line"><span style="color:#96D0FF">						&#x3C;button type="submit">Submit&#x3C;/button></span></span>
<span class="line"><span style="color:#96D0FF">					&#x3C;/form></span></span>
<span class="line"><span style="color:#96D0FF">					&#x3C;p>&#x3C;a href="/">Back to index&#x3C;/a>&#x3C;/p></span></span>
<span class="line"><span style="color:#96D0FF">				`</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#ADBAC7">      });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">      res.</span><span style="color:#DCBDFB">writeHead</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">200</span><span style="color:#ADBAC7">, { </span><span style="color:#96D0FF">"Content-Type"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"text/html"</span><span style="color:#ADBAC7"> });</span></span>
<span class="line"><span style="color:#ADBAC7">      res.</span><span style="color:#DCBDFB">end</span><span style="color:#ADBAC7">(html);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">      return</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">    }</span></span>
<span class="line"><span style="color:#ADBAC7">  })</span></span>
<span class="line"><span style="color:#ADBAC7">  .</span><span style="color:#DCBDFB">listen</span><span style="color:#ADBAC7">(port, () </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#ADBAC7">    console.</span><span style="color:#DCBDFB">log</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">`App is running on port ${</span><span style="color:#ADBAC7">port</span><span style="color:#96D0FF">}: http://localhost:${</span><span style="color:#ADBAC7">port</span><span style="color:#96D0FF">}`</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#ADBAC7">  });</span></span></code></pre>
<p>The most complex part of it is parsing the request body (<code>function parseBody</code>). It would be even shorter if we ignored multiple values with the same key.</p>
<p>And this is the complete <code>package.json</code>:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">{</span></span>
<span class="line"><span style="color:#8DDB8C">  "name"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"my-little-wep-app"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#8DDB8C">  "version"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"0.0.1"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#8DDB8C">  "type"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"module"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#8DDB8C">  "scripts"</span><span style="color:#ADBAC7">: {</span></span>
<span class="line"><span style="color:#8DDB8C">    "start"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"node index.js"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#8DDB8C">    "dev"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"node --watch index.js"</span></span>
<span class="line"><span style="color:#ADBAC7">  },</span></span>
<span class="line"><span style="color:#8DDB8C">  "engines"</span><span style="color:#ADBAC7">: {</span></span>
<span class="line"><span style="color:#8DDB8C">    "node"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">">= 18.11"</span></span>
<span class="line"><span style="color:#ADBAC7">  }</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
<p><code>npm run dev</code> will restart the server on changes to <code>index.js</code> or any of its potential imports. This is courtesy of Node.js <code>18.11+</code>&#39; <a href="https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch">native file watcher</a>.</p>
<p>I will never go back.</p>
]]></description></item><item><title>Custom Website Styles on iOS</title><link>https://blog.responsive.ch/custom-website-styles-on-ios/</link><guid isPermaLink="true">https://blog.responsive.ch/custom-website-styles-on-ios/</guid><pubDate>Sun, 08 Oct 2023 00:00:00 GMT</pubDate><description><![CDATA[<p>I tend to read in the evening. This is what I look like:</p>
<p><a href="https://giphy.com/gifs/natgeochannel-season-1-nat-geo-valley-of-the-boom-t6lAk9EJRLKajKTc3O"><img src="https://media.giphy.com/media/t6lAk9EJRLKajKTc3O/giphy.gif" alt=""></a></p>
<p>Yes, I have mounted my smartphone to the wall.</p>
<p>As websites like <a href="https://www.watson.ch">watson.ch</a> or <a href="https://www.vox.com">vox.com</a> don&#39;t seem to offer a dark mode, writing my own custom styles is the quickest solution.</p>
<p>Turns out there is a nice solution for that: <a href="https://github.com/quoid/userscripts">Userscripts</a>:</p>
<blockquote>
<p>Userscripts is an open source Safari extension that lets you save and run arbitrary bits of JavaScript (and CSS) code for the websites you visit. It implements a code editor right in your browser for a simple method of creating, editing and saving your code.</p>
</blockquote>
<p>This is what my custom styles for watson.ch look like:</p>
<details open>
	<summary>Toggle code</summary>

<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#768390">/* ==UserStyle==</span></span>
<span class="line"><span style="color:#768390">@name        Watson Dark Mode</span></span>
<span class="line"><span style="color:#768390">@description Light text on dark background for watson.ch</span></span>
<span class="line"><span style="color:#768390">@match       https://www.watson.ch/*</span></span>
<span class="line"><span style="color:#768390">==/UserStyle== */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/**</span></span>
<span class="line"><span style="color:#768390"> * 1. COLORS</span></span>
<span class="line"><span style="color:#768390"> */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6CB6FF">:root</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#F69D50">  --dark</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgb</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">43</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">43</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">43</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#F69D50">  --light</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgb</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#F69D50">  --light-secondary</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgba</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">0.75</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#F69D50">  --accent</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgb</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">244</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">15</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">151</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Default styles */</span></span>
<span class="line"><span style="color:#8DDB8C">body</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--dark</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--light</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Remove custom background */</span></span>
<span class="line"><span style="color:#6CB6FF">.cluster</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.region</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.teaser.teaser.teaser.teaser.teaser</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-footer</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__contextBoxList</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.combo_bg</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.mega_teaser_short_wrapper</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__tableOfContents__item</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__simple-infobox__box</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__result-table</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-teaser__image</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-ad</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__datawrapper</span><span style="color:#8DDB8C"> iframe</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background-color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--dark</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Remove border */</span></span>
<span class="line"><span style="color:#6CB6FF">.region</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.cluster</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-discussion</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.fat_wrapper</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  border-color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">transparent</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Remove custom font color */</span></span>
<span class="line"><span style="color:#6CB6FF">.text</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.text</span><span style="color:#8DDB8C"> h2</span><span style="color:#8DDB8C"> div</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.teaser_type_default.combo_fg</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__tableOfContents__item</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">inherit</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Optimize gradient colors */</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-storybreadcrumbs__fade</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background-image</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">linear-gradient</span><span style="color:#ADBAC7">(</span></span>
<span class="line"><span style="color:#F47067">    to</span><span style="color:#6CB6FF"> right</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">    transparent</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">    var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--dark</span><span style="color:#ADBAC7">)</span></span>
<span class="line"><span style="color:#ADBAC7">  ) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-next-story__actionbutton</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background-image</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">linear-gradient</span><span style="color:#ADBAC7">(</span></span>
<span class="line"><span style="color:#F47067">    to</span><span style="color:#6CB6FF"> bottom</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">    transparent</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">    var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--dark</span><span style="color:#ADBAC7">)</span></span>
<span class="line"><span style="color:#ADBAC7">  ) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Apply accent color where light/dark is not enough */</span></span>
<span class="line"><span style="color:#6CB6FF">.region</span><span style="color:#6CB6FF"> .mega_teaser_short_wrapper::before</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  border-bottom-color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--accent</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  top</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">-11</span><span style="color:#F47067">px</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/**</span></span>
<span class="line"><span style="color:#768390"> * 2. OPTIMIZATIONS</span></span>
<span class="line"><span style="color:#768390"> */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Set fixed height for ads to prevent constant reflow on scroll */</span></span>
<span class="line"><span style="color:#6CB6FF">.commercial</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  aspect-ratio</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">1</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  overflow</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">hidden</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  margin-bottom</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">1</span><span style="color:#F47067">rem</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Hide irrelevant elements */</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__contextBoxList</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-donation</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.story-recommendations</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__newsletter</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-little-boxes</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-next-story</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-sharebuttons</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-footer</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.cookiefooter</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.sorgometer</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">#donation</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-trending-topics-box</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-language-switch</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.weathersummary</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  display</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">none</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
</details>

<h2>Step by step</h2>
<ol>
<li><p>It starts with a <a href="https://github.com/openstyles/stylus/wiki/Writing-UserCSS">somewhat standardized</a> metadata block <sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>. An important part of it is specifying which pages the styles are supposed to be applied to.</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#768390">/* ==UserStyle==</span></span>
<span class="line"><span style="color:#768390"> @name        Watson Dark Mode</span></span>
<span class="line"><span style="color:#768390"> @description Light text on dark background for watson.ch</span></span>
<span class="line"><span style="color:#768390"> @match       https://www.watson.ch/*</span></span>
<span class="line"><span style="color:#768390"> ==/UserStyle== */</span></span></code></pre>
</li>
<li><p>Specification of the colors I intend to use as CSS custom properties:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#6CB6FF">:root</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#F69D50">  --dark</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgb</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">43</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">43</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">43</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#F69D50">  --light</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgb</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#F69D50">  --light-secondary</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgba</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">255</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">0.75</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#F69D50">  --accent</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">rgb</span><span style="color:#ADBAC7">(</span><span style="color:#6CB6FF">244</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">15</span><span style="color:#ADBAC7">, </span><span style="color:#6CB6FF">151</span><span style="color:#ADBAC7">);</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
</li>
<li><p>Application of the main colors to the body:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#8DDB8C">body</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--dark</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--light</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
</li>
<li><p>Removal of background, border and custom font colors on specific elements:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#6CB6FF">.cluster</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.region</span><span style="color:#768390"> /*,</span></span>
<span class="line"><span style="color:#768390">... more selectors */</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background-color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">transparent</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6CB6FF">.cluster</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.region</span><span style="color:#768390"> /*,</span></span>
<span class="line"><span style="color:#768390">... more selectors */</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  border-color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">transparent</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6CB6FF">.text</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.text</span><span style="color:#8DDB8C"> h2</span><span style="color:#8DDB8C"> div</span><span style="color:#768390"> /*,</span></span>
<span class="line"><span style="color:#768390">... more selectors */</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">inherit</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
</li>
<li><p>Other color optimizations on specific elements:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#768390">/* Optimize gradient colors */</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-storybreadcrumbs__fade</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  background-image</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">linear-gradient</span><span style="color:#ADBAC7">(</span></span>
<span class="line"><span style="color:#F47067">    to</span><span style="color:#6CB6FF"> right</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">    transparent</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">    var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--dark</span><span style="color:#ADBAC7">)</span></span>
<span class="line"><span style="color:#ADBAC7">  ) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"><span style="color:#768390">/* ... */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Apply accent color where light/dark is not enough */</span></span>
<span class="line"><span style="color:#6CB6FF">.region</span><span style="color:#6CB6FF"> .mega_teaser_short_wrapper::before</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  border-bottom-color</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">var</span><span style="color:#ADBAC7">(</span><span style="color:#F69D50">--accent</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">!important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  top</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">-11</span><span style="color:#F47067">px</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
</li>
<li><p>Other optimizations</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#768390">/* Set fixed height for ads to prevent constant reflow on scroll */</span></span>
<span class="line"><span style="color:#6CB6FF">.commercial</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  aspect-ratio</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">1</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  overflow</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">hidden</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#6CB6FF">  margin-bottom</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">1</span><span style="color:#F47067">rem</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">/* Hide irrelevant elements */</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-snippet__contextBoxList</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.watson-donation</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#6CB6FF">.story-recommendations</span><span style="color:#768390"> /*,</span></span>
<span class="line"><span style="color:#768390">... more selectors */</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#6CB6FF">  display</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">none</span><span style="color:#F47067"> !important</span><span style="color:#ADBAC7">;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
</li>
</ol>
<h2>Process</h2>
<p>I use the Safari developer tools to inspect the page and decide what styles to apply to which element. Then I edit the styles in the Userscript extension page and reload the page.</p>
<p>The styles are synced from my desktop to my iPhone via <a href="https://github.com/quoid/userscripts#usage">iCloud sync</a>. <sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<h2>What is this fixed ad height in step 6 about? Ever heard of ad blockers?</h2>
<p>I tend not to block ads on pages where I regularly consume free content. I would prefer to pay for the content and not have ads, but nobody listens to me.</p>
<p>As long as they don&#39;t trigger reflows while I&#39;m reading, I can live with ads. This can usually be enfored with <code>aspect-ratio: ...</code> and <code>overflow: hidden</code>. Otherwise I enable every ad blocker I can find, of course. There are limits.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="visually-hidden">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>See <a href="https://github.com/quoid/userscripts/issues/215">GitHub issue</a> on the differences between what Userscripts supports and what alternative tools like Stylus do. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>Recently, this synchronization has proven to be somewhat unreliable. From time to time, I have to open the <a href="https://support.apple.com/en-us/HT206481">Files</a> app and make sure all the user styles are downloaded to my device. But this is probably due to my phone being low on storage rather than a general issue. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
]]></description></item><item><title>Free analytics with Fathom on Fly.io</title><link>https://blog.responsive.ch/free-analytics-with-fathom-lite-on-fly-io/</link><guid isPermaLink="true">https://blog.responsive.ch/free-analytics-with-fathom-lite-on-fly-io/</guid><pubDate>Sat, 19 Nov 2022 00:00:00 GMT</pubDate><description><![CDATA[<p>As I&#39;m interested in the number of visitors my own websites are seeing, I need some basic analytics tooling. By setting up my own instance of <a href="https://github.com/usefathom/fathom">Fathom Lite</a>, the open-source version of <a href="https://usefathom.com/">Fathom Analytics</a>, I think I can do this in a fairly privacy-preserving way.</p>
<p>Fathom Lite offers a <a href="https://hub.docker.com/r/usefathom/fathom/">pre-built Docker image</a>. This is great if you don&#39;t enjoy following <a href="https://github.com/usefathom/fathom/blob/master/docs/Installation%20instructions.md">installation instructions</a> consisting of more than one bullet point.</p>
<p>But where to deploy this image? While being able to use AWS, Azure and Google Cloud in my client projects, I try to avoid them whenever possible. As I&#39;m not really experienced with any of their Configuration as Code solutions, deploying a Docker image and assigning a custom domain would include 25 steps in either of their fun admin UIs.</p>
<p>Meet <a href="https://fly.io">Fly.io</a>:</p>
<blockquote>
<p>Fly.io runs apps close to users. We transmogrify Docker containers into Firecracker micro-VMs that run on our hardware around the world, and connect all of them to a global Anycast network that picks up requests from around the world and routes them to the nearest VM.</p>
</blockquote>
<p>They had me at transmogrify. But what I actually like the most about it: Deploying an image like the Fathom one takes about a minute (assuming <a href="https://fly.io/docs/hands-on/install-flyctl/">flyctl</a> has been installed and your are <a href="https://fly.io/docs/getting-started/log-in-to-fly/">logged in</a>).</p>
<h3>Basic setup</h3>
<ol>
<li><p>Create the application:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">flyctl</span><span style="color:#96D0FF"> launch</span><span style="color:#6CB6FF"> --image</span><span style="color:#96D0FF"> usefathom/fathom</span></span></code></pre>
<p>This will prompt us for a name and a region. The result is a configuration file with the following content:</p>
<details>
  <summary>fly.toml</summary>
  ```
  app = "APPLICATION_NAME"
  kill_signal = "SIGINT"
  kill_timeout = 5
  processes = []

<p>[build]
image = &quot;usefathom/fathom&quot;</p>
<p>[env]</p>
<p>[experimental]
allowed_public_ports = []
auto_rollback = true</p>
<p>[[services]]
http_checks = []
internal_port = 8080
processes = [&quot;app&quot;]
protocol = &quot;tcp&quot;
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = &quot;connections&quot;</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>  [[services.ports]]</span></span>
<span class="line"><span>    force_https = true</span></span>
<span class="line"><span>    handlers = ["http"]</span></span>
<span class="line"><span>    port = 80</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  [[services.ports]]</span></span>
<span class="line"><span>    handlers = ["tls", "http"]</span></span>
<span class="line"><span>    port = 443</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  [[services.tcp_checks]]</span></span>
<span class="line"><span>    grace_period = "1s"</span></span>
<span class="line"><span>    interval = "15s"</span></span>
<span class="line"><span>    restart_limit = 0</span></span>
<span class="line"><span>    timeout = "2s"</span></span>
<span class="line"><span>```</span></span></code></pre>
  </details>
</li>
<li><p>Deploy the application:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">flyctl</span><span style="color:#96D0FF"> deploy</span></span></code></pre>
<p>We can access it via <code>APPLICATION_NAME.fly.io</code>.</p>
</li>
<li><p>Now we want to assign a custom domain. We do that by adding a <code>CNAME</code> record to the domain with a value of <code>APPLICATION_NAME.fly.io</code> <sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> and then creating a certificate:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">flyctl</span><span style="color:#96D0FF"> certs</span><span style="color:#96D0FF"> create</span><span style="color:#96D0FF"> CUSTOM_DOMAIN</span></span></code></pre>
</li>
<li><p>Time to bring out the champagne: Our Docker image is deployed to our custom domain.</p>
</li>
</ol>
<p><img src="media/success.jpeg" alt="Frank raising a glass in the 'Always Sunny' episode of 'It's Always Sunny in Philadelphia'"></p>
<h3>Configuration</h3>
<p>As we don&#39;t want our analytics data to be public (everyone would be jealous of our dozens of visitors per year), we need to <a href="https://github.com/usefathom/fathom/blob/master/docs/Installation%20instructions.md#register-your-admin-user">create an admin user</a> in Fathom. To do this, we connect via SSH and execute their script:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">flyctl</span><span style="color:#96D0FF"> ssh</span><span style="color:#96D0FF"> console</span></span>
<span class="line"><span style="color:#F47067">></span><span style="color:#ADBAC7"> cd /app</span></span>
<span class="line"><span style="color:#F47067">></span><span style="color:#ADBAC7"> ./fathom user add --email</span><span style="color:#F47067">=</span><span style="color:#96D0FF">"EMAIL"</span><span style="color:#ADBAC7"> --password</span><span style="color:#F47067">=</span><span style="color:#96D0FF">"PASSWORD"</span></span></code></pre>
<p>Now we will be greeted with a login when accessing Fathom:</p>
<p><img src="media/login-screen.png" alt="Screenshot of Fathom Lite showing a login prompt"></p>
<h3>Data persistence</h3>
<p>Our current setup has a tiny issue: The analytics data is not persistent. By default, the Fathom Lite image creates an SQLite database in <code>/app/fathom.db</code>. So whenever the app is redeployed, we lose our data. But instead of hooking up a <a href="https://github.com/usefathom/fathom/blob/master/docs/Configuration.md#accepted-values--defaults">MySQL or Postgres database</a>, we&#39;ll just make the SQLite database is (semi-)persistent. Fly.io has a great solution for this: <a href="https://fly.io/docs/reference/volumes/">Volumes</a>.</p>
<ol>
<li><p>Let&#39;s create a volume in the same region where we set up our app <sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">fly</span><span style="color:#96D0FF"> volumes</span><span style="color:#96D0FF"> create</span><span style="color:#96D0FF"> fathom_data</span><span style="color:#6CB6FF"> --region</span><span style="color:#96D0FF"> REGION</span><span style="color:#6CB6FF"> --size</span><span style="color:#6CB6FF"> 1</span></span></code></pre>
</li>
<li><p>Mount the volume into our image by adding the following lines to <code>fly.toml</code>:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">[</span><span style="color:#F69D50">mounts</span><span style="color:#ADBAC7">]</span></span>
<span class="line"><span style="color:#ADBAC7">  source = </span><span style="color:#96D0FF">"fathom_data"</span></span>
<span class="line"><span style="color:#ADBAC7">  destination = </span><span style="color:#96D0FF">"/app-temp"</span></span></code></pre>
</li>
<li><p>Redeploy:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">flyctl</span><span style="color:#96D0FF"> deploy</span></span></code></pre>
</li>
<li><p>Copy everything from <code>/app</code> to <code>/app-temp</code>:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">flyctl</span><span style="color:#96D0FF"> ssh</span><span style="color:#96D0FF"> console</span></span>
<span class="line"><span style="color:#F47067">></span><span style="color:#ADBAC7"> cp -R /app/. /app-temp</span></span></code></pre>
<p>This will copy the <code>fathom</code> executable and the <code>fathom.db</code> SQLite database to our mounted volume.</p>
</li>
<li><p>Change the mounting destination in <code>fly.toml</code>:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">[</span><span style="color:#F69D50">mounts</span><span style="color:#ADBAC7">]</span></span>
<span class="line"><span style="color:#ADBAC7">  source = </span><span style="color:#96D0FF">"fathom_data"</span></span>
<span class="line"><span style="color:#ADBAC7">  destination = </span><span style="color:#96D0FF">"/app"</span></span></code></pre>
</li>
<li><p>Redeploy:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">flyctl</span><span style="color:#96D0FF"> deploy</span></span></code></pre>
</li>
</ol>
<p>That&#39;s it! Our data will live through redeployments.</p>
<h3>Download database</h3>
<p>Now the &quot;semi&quot; part of (semi-)persistent is <em>really</em> important. As written in their <a href="https://fly.io/blog/persistent-storage-and-fast-remote-builds/">announcement</a>:</p>
<blockquote>
<p>Right now, for raw Fly volumes, resilience is your problem. There! I said it!</p>
</blockquote>
<p>There are <a href="https://fly.io/docs/reference/volumes/#snapshots-and-restores">snapshots</a> from the last five days. As I don&#39;t care too much about my analytics data, I can live with this level of resilience. But I might create the occasional offline backup.</p>
<p>To back up something from our volume, we use <code>scp</code> or a GUI like <a href="https://panic.com/transmit/">Transmit</a>. If you are like me and did not read the <a href="https://fly.io/docs/reference/private-networking/#private-network-vpn">WireGuard manual</a>, you might want do this via a proxy:</p>
<ol>
<li><p>Issue SSH credentials and create the proxy:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>flyctl ssh issue --agent</span></span>
<span class="line"><span>flyctl proxy 10022:22</span></span></code></pre>
</li>
<li><p>Download SQLite database:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>scp -P 10022 root@localhost:/app/fathom.db .</span></span></code></pre>
</li>
</ol>
<p>Now we are prepared for the <a href="https://community.fly.io/t/cant-load-site-anymore/8639">occasional data loss</a>.</p>
<p><img src="media/prepared.gif" alt="Charlie dressed with what he imagines would be survival gear in episode of 'It's Always Sunny in Philadelphia'"></p>
<h3>Pricing</h3>
<p>Fly.io&#39;s <a href="https://fly.io/docs/about/pricing/#free-allowances">free plan</a> has got us covered here.</p>
<h3>Random notes</h3>
<ul>
<li>I ❤️ Fly.io. If an app requires more than what <a href="https://vercel.com">Vercel</a> offers, I deploy it to Fly. They offer a great service and have a refreshing way of doing <a href="https://fly.io/blog/">marketing</a> and <a href="https://fly.io/docs/hiring/hiring/">recruiting</a>.</li>
<li>I&#39;m using their most basic functionality only. See <a href="https://fly.io/docs/">docs</a> for all the fancy things they can do.</li>
<li>Their <a href="https://community.fly.io">community Discourse</a> is helpful. It is often the best way to find specific information as the documentation does not cover everything. Specific example: I could not find documentation on how to download data from a volume, but reading through a few Discourse posts helped me figure it out at some point. <em>Important</em>: Don&#39;t forget to write how much you like DNSSEC and mention <a href="https://community.fly.io/u/thomas/">thomas</a>, this will give you priority support.</li>
</ul>
<br />
<small>
  Photo credits: [FX
  Networks](https://www.fxnetworks.com/shows/its-always-sunny-in-philadelphia) and [Yarn](https://www.getyarn.io/yarn-clip/6bcc1af5-1f8d-4131-8cfc-e429cbcdb5ad/gif).
</small>

<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="visually-hidden">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>See details and alternatives to CNAME in the <a href="https://fly.io/docs/app-guides/custom-domains-with-fly/">documentation</a>. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>In case you forgot: Use <code>flyctl status</code> or the Fly.io admin UI to find the region your app is currently deployed in. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
]]></description></item><item><title>Moving to Astro</title><link>https://blog.responsive.ch/moving-to-astro/</link><guid isPermaLink="true">https://blog.responsive.ch/moving-to-astro/</guid><pubDate>Fri, 18 Nov 2022 00:00:00 GMT</pubDate><description><![CDATA[<p>Blogs are great. They are my main source of (tech) information and I very much enjoy following people like <a href="https://blog.jim-nielsen.com">Jim Nielsen</a>, <a href="https://daverupert.com">Dave Rupert</a> or <a href="https://simonwillison.net">Simon Willison</a>. Their feeds have a cozy home in my <a href="https://reederapp.com">RSS reader</a>.</p>
<p>When I decided to do more writing of my own, the first step was crystal clear: Replace the underlying framework of this blog so the actual writing part can be postponed.</p>
<p>As a responsible web developer, I&#39;m obligated to spend at least 75% of my time evaluating new web frameworks. And there is no better way to do this than using them on <del>important customer</del> personal projects. When setting this blog up two years ago, <a href="https://www.gatsbyjs.com">Gastby</a> was shiny. It worked out fine and I learned the basics about the framework. (What I took away from it was that it felt a bit over-engineered.)</p>
<p>Getting back to it after more than a year and on a computer with a different processor architecture, I was obviously unable to run <code>npm install</code> without a few cheeky errors and warnings. As upgrading to the latest version would have meant following <a href="https://www.gatsbyjs.com/docs/reference/release-notes/migrating-from-v2-to-v3/">three</a> <a href="https://www.gatsbyjs.com/docs/reference/release-notes/migrating-source-plugin-from-v3-to-v4/">different</a> <a href="https://www.gatsbyjs.com/docs/reference/release-notes/migrating-from-v4-to-v5/">migration guides</a> in sequence, I just picked up a new shiny object: <a href="https://astro.build">Astro</a>.</p>
<p>I had been closely following Astro&#39;s development and very much sympathized with its general philosophy:</p>
<blockquote>
<p>[...] cover a wide range of styles in the noise field, from space music to psychedelically-tinged harsh noise</p>
</blockquote>
<p>Never mind, <a href="https://en.wikipedia.org/wiki/Astro_(Japanese_band)">wrong Astro</a>. This one here:</p>
<blockquote>
<p>Zero JavaScript Runtime. Astro renders HTML on the server and strips away any remaining, unused JavaScript.</p>
</blockquote>
<p>Now you might think this is irrelevant for a blog with zero client-side JavaScript to begin with. Great thinking! However, there are frameworks whose main purpose is to generate JavaScript out of no JavaScript so they can send it to the visitors of your website because they hopefully are JavaScript afficionados, too.</p>
<h3>My Astro Takeaways</h3>
<ol>
<li><p>It&#39;s fun! Even the installation process, thanks to <a href="https://twitter.com/n_moore/status/1567164215307149312">Nate Moore</a>&#39;s great work. <sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>
<img src="media/prompt.png" alt="create-astro saying hi to me"></p>
</li>
<li><p>There are less dependencies:</p>
<ul>
<li><code>node_modules</code> before: <code>479.8 MB for 59&#39;083 items</code></li>
<li><code>node_modules</code> after: <code>242.1 MB for 10&#39;110 items</code></li>
</ul>
<p>As everything below 250 MB does not count as a real node_modules folder by law, we basically have no dependencies.</p>
</li>
<li><p>I&#39;m missing the possibility to add the complete post to the <a href="/rss.xml">RSS feed</a> (currently a limitation of <a href="https://github.com/withastro/astro/pull/5366#pullrequestreview-1179439896">using MDX</a>).</p>
</li>
<li><p>The community is really welcoming. When I <a href="https://github.com/withastro/astro/pull/5427">opened a PR</a> to add support for footnote customizations in Astro&#39;s MDX integration, <a href="https://www.rainsberger.ca/">Sarah Rainsberger</a> and <a href="https://bholmes.dev">Ben Holmes</a> <sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> were very helpful and kind.</p>
</li>
</ol>
<h3>Further reading</h3>
<ul>
<li>One of Astro&#39;s main strengths is dealing with any kind of client-side JavaScript framework in a performant way. See the <a href="https://astro.build/blog/introducing-astro/">introduction post</a> from last year and the current <a href="https://docs.astro.build/en/concepts/islands/">Astro Island documentation</a> for details.</li>
<li>Taking this further, Fernando Doglio&#39;s <a href="https://blog.bitsrc.io/playing-with-astro-sharing-state-between-react-and-vue-components-2d5abc89f4b4">article</a> demonstrates a way of sharing state between island components written in different frameworks.</li>
</ul>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="visually-hidden">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>We were lucky enough to have Nate speak at this year&#39;s <a href="https://frontconference.com/speakers/nate-moore">Front Conference</a> in Zürich. Turns out he knows more about the city&#39;s fountains than every living resident. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>Ben came up with a <em>really</em> fun way to present tech topics on a whiteboard, check out <a href="https://wtw.dev/">#WhiteboardtheWeb videos</a>! <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
]]></description></item><item><title>Setting up a Mac</title><link>https://blog.responsive.ch/setting-up-a-mac/</link><guid isPermaLink="true">https://blog.responsive.ch/setting-up-a-mac/</guid><pubDate>Sat, 13 Feb 2021 00:00:00 GMT</pubDate><description><![CDATA[<p>First thing I do is some login screen gardening: Displaying my best self-portrait and <a href="https://support.apple.com/en-bn/guide/mac-help/mh35890/mac">adding contact information</a>. This way, I don&#39;t have to remember taking my MacBook with me when leaving the coffee shop.</p>
<p><img src="media/login.png" alt="Login screen with white avatar, name and phone number"></p>
<p>The first app I install is <a href="https://1password.com">1Password</a>. It allows me to log into Github to download my <code>.zshrc</code>, <code>.gitconfig</code> and other preference files. Like the cool kids, I started out with a setup script. However, I always forgot to maintain it and ended up with outdated preferences. Now, I just push the system and application preferences I care about (e.g. <code>com.apple.dock.plist</code>) to Github right before setting up a new machine.</p>
<p>At some point I realize I have to install the <a href="https://developer.apple.com/xcode/features/#:~:text=Command%20Line%20Tools">Xcode Command Line Tools</a> for Git to work. Depending on whether I update macOS sometime in between, I&#39;m doing this multiple times as Apple pretends to lose them with every new version. Note to self: <code>xcode-select --reset</code> will usually be a quicker solution than the often recommended deleting an re-installing.</p>
<p>The second app I can&#39;t live without is <a href="https://www.alfredapp.com">Alfred</a>. Highly recommended by Batman, Alfred not only helps me launching applications, but also remembering my <a href="https://www.alfredapp.com/help/features/clipboard/">clipboard history</a>, showing <a href="https://github.com/jeppestaerk/alfred-show-network-info">my current IP</a> or finding <a href="https://github.com/SamVerschueren/alfred-fkill">processes to kill</a>. A typical workflow while writing frontend code:</p>
<ol>
<li><p>Opening the docs for a specific feature in <a href="https://kapeli.com/dash">Dash</a>:</p>
<p><img src="media/alfred-dash.png" alt="Dash integration in Alfred"></p>
</li>
<li><p>Checking its <a href="https://github.com/willfarrell/alfred-caniuse-workflow">browser support</a>:</p>
<p><img src="media/alfred-caniuse.png" alt="Can i use integration in Alfred"></p>
</li>
<li><p>Finding a polyfill on <a href="https://github.com/sindresorhus/alfred-npms">npm</a>:</p>
<p><img src="media/alfred-npm.png" alt="npm package search in Alfred"></p>
</li>
</ol>
<p>The third one is <a href="https://rectangleapp.com">Rectangle</a>. It is used by <a href="https://github.com/rxhanson/Rectangle/commits/master?before=57ac4a51bbb3cd0250f6bbf1d0e23573a2e6dd80+35&branch=master">fellow CEOs</a> and allows me to move around windows via keyboard (or mouse). When working on a widescreen display, I usually want to have two windows next to eachother. So whenever I open an app, I press <kbd>^⌥←</kbd> or <kbd>^⌥→</kbd> to use one or the other screen half. Another important use-case is catching up with SNL in an efficient way:</p>
<p><img src="media/snl.png" alt="Four windows displaying the SNL YouTube channel"></p>
<p>Moving on to the menu bar, I use <a href="https://sindresorhus.com/dato">Dato</a> to display the current week number and have immediate access to the calendar. As you can see, I&#39;m very good at calendaring:</p>
<p><img src="media/dato.png" alt="Screenshot of expanded calendar view in Dato"></p>
<p>The other menu bar app I approve of is <a href="https://sindresorhus.com/lungo">Lungo</a>. Why should your Mac be able to sleep when you can&#39;t?</p>
<p>After having installed <a href="https://www.docker.com">Docker</a>, <a href="https://brew.sh">Homebrew</a> and <a href="https://github.com/nvm-sh/nvm">nvm</a>, it&#39;s time for some atypically large <a href="https://www.electronjs.org">electrons</a>: <a href="https://code.visualstudio.com">VS Code</a>, <a href="https://slack.com">Slack</a> and <a href="https://www.notion.so">Notion</a> are the applications I spend most of my screen time in. I very much enjoy the fact that they are written in JavaScript. While, like any reasonable Swiss citizen, I have mixed feelings about some of them being developed in <a href="https://channel9.msdn.com/Events/TechDays/TechDays16Baden/Keynote-1-Visual-Studio-Code-made-in-Switzerland">Zürich</a>.</p>
<p>Last thing to do is setting up <a href="https://www.arqbackup.com">Arq</a> to back up my files into a <a href="https://aws.amazon.com/glacier/">glacier</a>, and installing <a href="https://www.dropbox.com">Dropbox</a> to gain access to everything not living in a Git repository.</p>
]]></description></item><item><title>Fun with polyfills</title><link>https://blog.responsive.ch/fun-with-polyfills/</link><guid isPermaLink="true">https://blog.responsive.ch/fun-with-polyfills/</guid><pubDate>Thu, 31 Dec 2020 00:00:00 GMT</pubDate><description><![CDATA[<p>Using new JavaScript syntax or features while still supporting older browsers requires us to transpile and polyfill our code before deployment. The tool of choice is usually <a href="https://babeljs.io/">Babel</a>. Its <a href="https://babeljs.io/docs/en/babel-preset-env">preset-env</a> plugin hides most of the complexity and allows us to simply specify a list of browsers to support, sit back and enjoy the ride. However, the ride might take an unexpected turn when analyzing the generated code.</p>
<p><img src="media/ride.png" alt="Dee and Dennis on a ride in episode 'The Gang Goes to the Jersey Shore' of 'It's Always Sunny in Philadelphia"></p>
<p>Let&#39;s say we want to log all links on a page:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F47067">const</span><span style="color:#6CB6FF"> links</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> document.</span><span style="color:#DCBDFB">querySelectorAll</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"a"</span><span style="color:#ADBAC7">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">links.</span><span style="color:#DCBDFB">forEach</span><span style="color:#ADBAC7">((</span><span style="color:#F69D50">link</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#ADBAC7">  console.</span><span style="color:#DCBDFB">log</span><span style="color:#ADBAC7">(link.href);</span></span>
<span class="line"><span style="color:#ADBAC7">});</span></span></code></pre>
<p>For this to work in IE 11, we would want to rewrite it to the following code:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#768390">// Polyfill missing `.forEach` on `NodeList`</span></span>
<span class="line"><span style="color:#F47067">if</span><span style="color:#ADBAC7"> (window.NodeList </span><span style="color:#F47067">&#x26;&#x26;</span><span style="color:#F47067"> !</span><span style="color:#6CB6FF">NodeList</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach) {</span></span>
<span class="line"><span style="color:#6CB6FF">  NodeList</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach </span><span style="color:#F47067">=</span><span style="color:#6CB6FF"> Array</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">// Use `var` over `const`</span></span>
<span class="line"><span style="color:#F47067">var</span><span style="color:#ADBAC7"> links </span><span style="color:#F47067">=</span><span style="color:#ADBAC7"> document.</span><span style="color:#DCBDFB">querySelectorAll</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"a"</span><span style="color:#ADBAC7">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">// Replace arrow function</span></span>
<span class="line"><span style="color:#ADBAC7">links.</span><span style="color:#DCBDFB">forEach</span><span style="color:#ADBAC7">(</span><span style="color:#F47067">function</span><span style="color:#ADBAC7"> (</span><span style="color:#F69D50">link</span><span style="color:#ADBAC7">) {</span></span>
<span class="line"><span style="color:#ADBAC7">  console.</span><span style="color:#DCBDFB">log</span><span style="color:#ADBAC7">(link.href);</span></span>
<span class="line"><span style="color:#ADBAC7">});</span></span></code></pre>
<p>Babel slightly disagrees and rewrites it to the following code instead:</p>
<details open>
	<summary>Toggle code</summary>

<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>!function(){var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function n(t){var n={exports:{}};return t(n,n.exports),n.exports}function u(t){try{return!!t()}catch(t){return!0}}function r(t,n){return{enumerable:!(1&#x26;t),configurable:!(2&#x26;t),writable:!(4&#x26;t),value:n}}function e(t){return S.call(t).slice(8,-1)}function m(t){if(null==t)throw TypeError("Can't call method on "+t);return t}function f(t){return w(m(t))}function o(t){return"object"==typeof t?null!==t:"function"==typeof t}function i(t,n){if(!o(t))return t;var e,r;if(n&#x26;&#x26;"function"==typeof(e=t.toString)&#x26;&#x26;!o(r=e.call(t)))return r;if("function"==typeof(e=t.valueOf)&#x26;&#x26;!o(r=e.call(t)))return r;if(!n&#x26;&#x26;"function"==typeof(e=t.toString)&#x26;&#x26;!o(r=e.call(t)))return r;throw TypeError("Can't convert object to primitive value")}function a(t,n){return b.call(t,n)}function c(t){if(!o(t))throw TypeError(String(t)+" is not an object");return t}function l(n,e){try{P(s,n,e)}catch(t){s[n]=e}return e}var s=(H=function(t){return t&#x26;&#x26;t.Math==Math&#x26;&#x26;t})("object"==typeof globalThis&#x26;&#x26;globalThis)||H("object"==typeof window&#x26;&#x26;window)||H("object"==typeof self&#x26;&#x26;self)||H("object"==typeof t&#x26;&#x26;t)||function(){return this}()||Function("return this")(),p=!u(function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}),y={}.propertyIsEnumerable,h=Object.getOwnPropertyDescriptor,g={f:h&#x26;&#x26;!y.call({1:2},1)?function(t){t=h(this,t);return!!t&#x26;&#x26;t.enumerable}:y},S={}.toString,d="".split,w=u(function(){return!Object("z").propertyIsEnumerable(0)})?function(t){return"String"==e(t)?d.call(t,""):Object(t)}:Object,b={}.hasOwnProperty,v=s.document,L=o(v)&#x26;&#x26;o(v.createElement),O=!p&#x26;&#x26;!u(function(){return 7!=Object.defineProperty(L?v.createElement("div"):{},"a",{get:function(){return 7}}).a}),E=Object.getOwnPropertyDescriptor,T={f:p?E:function(t,n){if(t=f(t),n=i(n,!0),O)try{return E(t,n)}catch(t){}if(a(t,n))return r(!g.f.call(t,n),t[n])}},j=Object.defineProperty,M={f:p?j:function(t,n,e){if(c(t),n=i(n,!0),c(e),O)try{return j(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported");return"value"in e&#x26;&#x26;(t[n]=e.value),t}},P=p?function(t,n,e){return M.f(t,n,r(1,e))}:function(t,n,e){return t[n]=e,t},A=s[H="__core-js_shared__"]||l(H,{}),C=Function.toString;function x(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++q+z).toString(36)}"function"!=typeof A.inspectSource&#x26;&#x26;(A.inspectSource=function(t){return C.call(t)});var k,N,D,G,I,V,F,R,_=A.inspectSource,y="function"==typeof(t=s.WeakMap)&#x26;&#x26;/native code/.test(_(t)),H=n(function(t){(t.exports=function(t,n){return A[t]||(A[t]=void 0!==n?n:{})})("versions",[]).push({version:"3.8.1",mode:"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})}),q=0,z=Math.random(),W=H("keys"),B={},t=s.WeakMap;function K(t){return"function"==typeof t?t:void 0}function Y(t){return isNaN(t=+t)?0:(0&#x3C;t?ut:it)(t)}function J(t){return 0&#x3C;t?ct(Y(t),9007199254740991):0}function Q(r,o,t){if(!function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function")}(r),void 0===o)return r;switch(t){case 0:return function(){return r.call(o)};case 1:return function(t){return r.call(o,t)};case 2:return function(t,n){return r.call(o,t,n)};case 3:return function(t,n,e){return r.call(o,t,n,e)}}return function(){return r.apply(o,arguments)}}function U(t,n){var e;return Lt(t)&#x26;&#x26;("function"==typeof(e=t.constructor)&#x26;&#x26;(e===Array||Lt(e.prototype))||o(e)&#x26;&#x26;null===(e=e[Tt]))&#x26;&#x26;(e=void 0),new(void 0===e?Array:e)(0===n?0:n)}function X(t){throw t}F=y?(k=A.state||(A.state=new t),N=k.get,D=k.has,G=k.set,I=function(t,n){return n.facade=t,G.call(k,t,n),n},V=function(t){return N.call(k,t)||{}},function(t){return D.call(k,t)}):(R=W[Z="state"]||(W[Z]=x(Z)),B[R]=!0,I=function(t,n){return n.facade=t,P(t,R,n),n},V=function(t){return a(t,R)?t[R]:{}},function(t){return a(t,R)});var Z,$,tt,nt,et={set:I,get:V,has:F,enforce:function(t){return F(t)?V(t):I(t,{})},getterFor:function(e){return function(t){var n;if(!o(t)||(n=V(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}},rt=n(function(t){var n=et.get,c=et.enforce,f=String(String).split("String");(t.exports=function(t,n,e,r){var o=!!r&#x26;&#x26;!!r.unsafe,i=!!r&#x26;&#x26;!!r.enumerable,u=!!r&#x26;&#x26;!!r.noTargetGet;"function"==typeof e&#x26;&#x26;("string"!=typeof n||a(e,"name")||P(e,"name",n),(r=c(e)).source||(r.source=f.join("string"==typeof n?n:""))),t!==s?(o?!u&#x26;&#x26;t[n]&#x26;&#x26;(i=!0):delete t[n],i?t[n]=e:P(t,n,e)):i?t[n]=e:l(n,e)})(Function.prototype,"toString",function(){return"function"==typeof this&#x26;&#x26;n(this).source||_(this)})}),ot=s,it=Math.ceil,ut=Math.floor,ct=Math.min,ft=Math.max,at=Math.min,lt={includes:(t=function(c){return function(t,n,e){var r,o=f(t),i=J(o.length),u=function(t,n){t=Y(t);return t&#x3C;0?ft(t+n,0):at(t,n)}(e,i);if(c&#x26;&#x26;n!=n){for(;u&#x3C;i;)if((r=o[u++])!=r)return!0}else for(;u&#x3C;i;u++)if((c||u in o)&#x26;&#x26;o[u]===n)return c||u||0;return!c&#x26;&#x26;-1}})(!0),indexOf:t(!1)}.indexOf,st=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"].concat("length","prototype"),pt={f:Object.getOwnPropertyNames||function(t){return function(t,n){var e,r=f(t),o=0,i=[];for(e in r)!a(B,e)&#x26;&#x26;a(r,e)&#x26;&#x26;i.push(e);for(;n.length>o;)a(r,e=n[o++])&#x26;&#x26;(~lt(i,e)||i.push(e));return i}(t,st)}},yt={f:Object.getOwnPropertySymbols},ht=function(t,n){return arguments.length&#x3C;2?K(ot[t])||K(s[t]):ot[t]&#x26;&#x26;ot[t][n]||s[t]&#x26;&#x26;s[t][n]}("Reflect","ownKeys")||function(t){var n=pt.f(c(t)),e=yt.f;return e?n.concat(e(t)):n},gt=/#|\.prototype\./,St=(t=function(t,n){t=dt[St(t)];return t==vt||t!=bt&#x26;&#x26;("function"==typeof n?u(n):!!n)}).normalize=function(t){return String(t).replace(gt,".").toLowerCase()},dt=t.data={},bt=t.NATIVE="N",vt=t.POLYFILL="P",mt=t,wt=T.f,Lt=Array.isArray||function(t){return"Array"==e(t)},Ot=!!Object.getOwnPropertySymbols&#x26;&#x26;!u(function(){return!String(Symbol())}),t=Ot&#x26;&#x26;!Symbol.sham&#x26;&#x26;"symbol"==typeof Symbol.iterator,Et=H("wks"),y=s.Symbol,W=t?y:y&#x26;&#x26;y.withoutSetter||x,Tt=(a(Et,Z="species")||(Ot&#x26;&#x26;a(y,Z)?Et[Z]=y[Z]:Et[Z]=W("Symbol."+Z)),Et[Z]),jt=[].push,t={forEach:(H=function(p){var y=1==p,h=2==p,g=3==p,S=4==p,d=6==p,b=7==p,v=5==p||d;return function(t,n,e,r){for(var o,i,u=Object(m(t)),c=w(u),f=Q(n,e,3),a=J(c.length),l=0,r=r||U,s=y?r(t,a):h||b?r(t,0):void 0;l&#x3C;a;l++)if((v||l in c)&#x26;&#x26;(i=f(o=c[l],l,u),p))if(y)s[l]=i;else if(i)switch(p){case 3:return!0;case 5:return o;case 6:return l;case 2:jt.call(s,o)}else switch(p){case 4:return!1;case 7:jt.call(s,o)}return d?-1:g||S?S:s}})(0),map:H(1),filter:H(2),some:H(3),every:H(4),find:H(5),findIndex:H(6),filterOut:H(7)},Mt=Object.defineProperty,Pt={},At=t.forEach,H=!!(tt=[]["forEach"])&#x26;&#x26;u(function(){tt.call(null,$||function(){throw 1},1)}),t=function(t,n){if(a(Pt,t))return Pt[t];var e=[][t],r=!!a(n=n||{},"ACCESSORS")&#x26;&#x26;n.ACCESSORS,o=a(n,0)?n[0]:X,i=a(n,1)?n[1]:void 0;return Pt[t]=!!e&#x26;&#x26;!u(function(){if(r&#x26;&#x26;!p)return 1;var t={length:-1};r?Mt(t,1,{enumerable:!0,get:X}):t[1]=1,e.call(t,o,i)})}("forEach"),Ct=H&#x26;&#x26;t?[].forEach:function(t){return At(this,t,1&#x3C;arguments.length?arguments[1]:void 0)};for(nt in!function(t,n){var e,r,o,i=t.target,u=t.global,c=t.stat,f=u?s:c?s[i]||l(i,{}):(s[i]||{}).prototype;if(f)for(e in n){if(r=n[e],o=t.noTargetGet?(o=wt(f,e))&#x26;&#x26;o.value:f[e],!mt(u?e:i+(c?".":"#")+e,t.forced)&#x26;&#x26;void 0!==o){if(typeof r==typeof o)continue;!function(t,n){for(var e=ht(n),r=M.f,o=T.f,i=0;i&#x3C;e.length;i++){var u=e[i];a(t,u)||r(t,u,o(n,u))}}(r,o)}(t.sham||o&#x26;&#x26;o.sham)&#x26;&#x26;P(r,"sham",!0),rt(f,e,r,t)}}({target:"Array",proto:!0,forced:[].forEach!=Ct},{forEach:Ct}),{CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}){var xt=s[nt],kt=xt&#x26;&#x26;xt.prototype;if(kt&#x26;&#x26;kt.forEach!==Ct)try{P(kt,"forEach",Ct)}catch(t){kt.forEach=Ct}}document.querySelectorAll("a").forEach(function(t){console.log(t.href)})}();!function(){var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function v(r,o,t){if(!function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function")}(r),void 0===o)return r;switch(t){case 0:return function(){return r.call(o)};case 1:return function(t){return r.call(o,t)};case 2:return function(t,e){return r.call(o,t,e)};case 3:return function(t,e,n){return r.call(o,t,e,n)}}return function(){return r.apply(o,arguments)}}function u(t){try{return!!t()}catch(t){return!0}}function e(t){return y.call(t).slice(8,-1)}function L(t){return Object(function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}(t))}function w(t){return 0&#x3C;t?d((t=t,isNaN(t=+t)?0:(0&#x3C;t?S:p)(t)),9007199254740991):0}function o(t){return"object"==typeof t?null!==t:"function"==typeof t}function r(t){if(!o(t))throw TypeError(String(t)+" is not an object");return t}function c(t,e){return V.call(t,e)}function T(t,e){var n;return b(t)&#x26;&#x26;("function"==typeof(n=t.constructor)&#x26;&#x26;(n===Array||b(n.prototype))||o(n)&#x26;&#x26;null===(n=n[H]))&#x26;&#x26;(n=void 0),new(void 0===n?Array:n)(0===e?0:e)}function f(t){throw t}var n,i,l,a,s=(G=function(t){return t&#x26;&#x26;t.Math==Math&#x26;&#x26;t})("object"==typeof globalThis&#x26;&#x26;globalThis)||G("object"==typeof window&#x26;&#x26;window)||G("object"==typeof self&#x26;&#x26;self)||G("object"==typeof t&#x26;&#x26;t)||function(){return this}()||Function("return this")(),y={}.toString,h="".split,E=u(function(){return!Object("z").propertyIsEnumerable(0)})?function(t){return"String"==e(t)?h.call(t,""):Object(t)}:Object,p=Math.ceil,S=Math.floor,d=Math.min,b=Array.isArray||function(t){return"Array"==e(t)},g=!u(function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}),m=s.document,O=o(m)&#x26;&#x26;o(m.createElement),j=!g&#x26;&#x26;!u(function(){return 7!=Object.defineProperty(O?m.createElement("div"):{},"a",{get:function(){return 7}}).a}),M=Object.defineProperty,C={f:g?M:function(t,e,n){if(r(t),e=function(t,e){if(!o(t))return t;var n,r;if(e&#x26;&#x26;"function"==typeof(n=t.toString)&#x26;&#x26;!o(r=n.call(t)))return r;if("function"==typeof(n=t.valueOf)&#x26;&#x26;!o(r=n.call(t)))return r;if(!e&#x26;&#x26;"function"==typeof(n=t.toString)&#x26;&#x26;!o(r=n.call(t)))return r;throw TypeError("Can't convert object to primitive value")}(e,!0),r(n),j)try{return M(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&#x26;&#x26;(t[e]=n.value),t}},A=g?function(t,e,n){return C.f(t,e,{enumerable:!((e=1)&#x26;e),configurable:!(2&#x26;e),writable:!(4&#x26;e),value:n})}:function(t,e,n){return t[e]=n,t},P=s[G="__core-js_shared__"]||function(e,n){try{A(s,e,n)}catch(t){s[e]=n}return n}(G,{}),t=(function(t){(t.exports=function(t,e){return P[t]||(P[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.8.1",mode:"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})}(n={exports:{}}),n.exports),V={}.hasOwnProperty,k=0,x=Math.random(),D=!!Object.getOwnPropertySymbols&#x26;&#x26;!u(function(){return!String(Symbol())}),G=D&#x26;&#x26;!Symbol.sham&#x26;&#x26;"symbol"==typeof Symbol.iterator,N=t("wks"),R=s.Symbol,_=G?R:R&#x26;&#x26;R.withoutSetter||function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++k+x).toString(36)},H=(c(N,n="species")||(D&#x26;&#x26;c(R,n)?N[n]=R[n]:N[n]=_("Symbol."+n)),N[n]),F=[].push,G={forEach:(t=function(y){var h=1==y,p=2==y,S=3==y,d=4==y,b=6==y,g=7==y,m=5==y||b;return function(t,e,n,r){for(var o,i,u=L(t),c=E(u),f=v(e,n,3),l=w(c.length),a=0,r=r||T,s=h?r(t,l):p||g?r(t,0):void 0;a&#x3C;l;a++)if((m||a in c)&#x26;&#x26;(i=f(o=c[a],a,u),y))if(h)s[a]=i;else if(i)switch(y){case 3:return!0;case 5:return o;case 6:return a;case 2:F.call(s,o)}else switch(y){case 4:return!1;case 7:F.call(s,o)}return b?-1:S||d?d:s}})(0),map:t(1),filter:t(2),some:t(3),every:t(4),find:t(5),findIndex:t(6),filterOut:t(7)},I=Object.defineProperty,q={},z=G.forEach,t=!!(l=[]["forEach"])&#x26;&#x26;u(function(){l.call(null,i||function(){throw 1},1)}),G=function(t,e){if(c(q,t))return q[t];var n=[][t],r=!!c(e=e||{},"ACCESSORS")&#x26;&#x26;e.ACCESSORS,o=c(e,0)?e[0]:f,i=c(e,1)?e[1]:void 0;return q[t]=!!n&#x26;&#x26;!u(function(){if(r&#x26;&#x26;!g)return 1;var t={length:-1};r?I(t,1,{enumerable:!0,get:f}):t[1]=1,n.call(t,o,i)})}("forEach"),B=t&#x26;&#x26;G?[].forEach:function(t){return z(this,t,1&#x3C;arguments.length?arguments[1]:void 0)};for(a in{CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}){var J=s[a],K=J&#x26;&#x26;J.prototype;if(K&#x26;&#x26;K.forEach!==B)try{A(K,"forEach",B)}catch(t){K.forEach=B}}document.querySelectorAll("a").forEach(function(t){console.log(t.href)})}();</span></span></code></pre>
</details>

<p>Why oh why? Let&#39;s see how we got there.</p>
<p><img src="media/analysis.png" alt="Mac explaining things in 'Reynolds vs. Reynolds: The Cereal Defense' of 'It's Always Sunny in Philadelphia'"></p>
<h2>Basic setup</h2>
<p>An exemplary Babel setup as specified in a <code>babel.config.js</code> file could look the following:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#6CB6FF">module</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">exports</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#ADBAC7">  presets: [</span></span>
<span class="line"><span style="color:#ADBAC7">    [</span></span>
<span class="line"><span style="color:#96D0FF">      "@babel/preset-env"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#ADBAC7">      {</span></span>
<span class="line"><span style="color:#768390">        // Which browsers to support</span></span>
<span class="line"><span style="color:#768390">        // See https://github.com/browserslist/browserslist</span></span>
<span class="line"><span style="color:#ADBAC7">        targets: </span><span style="color:#96D0FF">"> 0.5%, last 2 versions, Firefox ESR, not dead"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">        // Add polyfills where needed</span></span>
<span class="line"><span style="color:#768390">        // See https://babeljs.io/docs/en/babel-preset-env#usebuiltins-usage</span></span>
<span class="line"><span style="color:#ADBAC7">        useBuiltIns: </span><span style="color:#96D0FF">"usage"</span><span style="color:#ADBAC7">,</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">        // Use latest core-js</span></span>
<span class="line"><span style="color:#768390">        // See https://babeljs.io/docs/en/babel-preset-env#corejs</span></span>
<span class="line"><span style="color:#ADBAC7">        corejs: </span><span style="color:#6CB6FF">3</span><span style="color:#ADBAC7">,</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390">        // Log transforms and added polyfills</span></span>
<span class="line"><span style="color:#768390">        // See https://babeljs.io/docs/en/babel-preset-env#debug</span></span>
<span class="line"><span style="color:#ADBAC7">        debug: </span><span style="color:#6CB6FF">true</span><span style="color:#ADBAC7">,</span></span>
<span class="line"><span style="color:#ADBAC7">      },</span></span>
<span class="line"><span style="color:#ADBAC7">    ],</span></span>
<span class="line"><span style="color:#ADBAC7">  ],</span></span>
<span class="line"><span style="color:#ADBAC7">};</span></span></code></pre>
<p>The <code>targets</code> option is the most relevant one as it tells Babel which browsers we care about. It uses <a href="https://github.com/browserslist/browserslist">Browserslist</a> under the hood, allowing us to specify reasonable versions without the need to be very explicit. This prevents us from, shall we say, accidentally depriving <a href="https://investor.opera.com/news-releases/news-release-details/opera-news-sets-new-record-200-million-users">200 million Opera users</a> of the joy of using our fancy website since we forgot about that browser.</p>
<p><em>Note:</em> This configuration is usually extracted into a separate <code>.browserslistrc</code> file which can be used by other tools like <a href="https://github.com/postcss/autoprefixer">autoprefixer</a>, too.</p>
<p>Based on this list of browsers, Babel will decide which features to transpile to older syntax as well as which polyfills to include. The polyfills themselves are provided by <a href="https://github.com/zloirock/core-js">core-js</a>.</p>
<h2>Default output</h2>
<p>So what happens with our example? Babel logs the following debugging information:</p>
<details open>
	<summary>Toggle log</summary>

<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>Using targets:</span></span>
<span class="line"><span>{</span></span>
<span class="line"><span>	"android": "86",</span></span>
<span class="line"><span>	"chrome": "85",</span></span>
<span class="line"><span>	"edge": "86",</span></span>
<span class="line"><span>	"firefox": "78",</span></span>
<span class="line"><span>	"ie": "11",</span></span>
<span class="line"><span>	"ios": "12.2",</span></span>
<span class="line"><span>	"opera": "71",</span></span>
<span class="line"><span>	"safari": "13.1",</span></span>
<span class="line"><span>	"samsung": "12"</span></span>
<span class="line"><span>}</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Using modules transform: auto</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Using plugins:</span></span>
<span class="line"><span>	proposal-numeric-separator { "ie":"11", "ios":"12.2" }</span></span>
<span class="line"><span>	proposal-logical-assignment-operators { "edge":"86", "firefox":"78", "ie":"11", "ios":"12.2", "opera":"71", "safari":"13.1", "samsung":"12" }</span></span>
<span class="line"><span>	proposal-nullish-coalescing-operator { "ie":"11", "ios":"12.2", "samsung":"12" }</span></span>
<span class="line"><span>	proposal-optional-chaining { "ie":"11", "ios":"12.2", "samsung":"12" }</span></span>
<span class="line"><span>	proposal-json-strings { "ie":"11" }</span></span>
<span class="line"><span>	proposal-optional-catch-binding { "ie":"11" }</span></span>
<span class="line"><span>	transform-parameters { "ie":"11" }</span></span>
<span class="line"><span>	proposal-async-generator-functions { "ie":"11" }</span></span>
<span class="line"><span>	proposal-object-rest-spread { "ie":"11" }</span></span>
<span class="line"><span>	transform-dotall-regex { "ie":"11" }</span></span>
<span class="line"><span>	proposal-unicode-property-regex { "ie":"11" }</span></span>
<span class="line"><span>	transform-named-capturing-groups-regex { "ie":"11" }</span></span>
<span class="line"><span>	transform-async-to-generator { "ie":"11" }</span></span>
<span class="line"><span>	transform-exponentiation-operator { "ie":"11" }</span></span>
<span class="line"><span>	transform-template-literals { "ie":"11", "ios":"12.2" }</span></span>
<span class="line"><span>	transform-literals { "ie":"11" }</span></span>
<span class="line"><span>	transform-function-name { "ie":"11" }</span></span>
<span class="line"><span>	transform-arrow-functions { "ie":"11" }</span></span>
<span class="line"><span>	transform-classes { "ie":"11" }</span></span>
<span class="line"><span>	transform-object-super { "ie":"11" }</span></span>
<span class="line"><span>	transform-shorthand-properties { "ie":"11" }</span></span>
<span class="line"><span>	transform-duplicate-keys { "ie":"11" }</span></span>
<span class="line"><span>	transform-computed-properties { "ie":"11" }</span></span>
<span class="line"><span>	transform-for-of { "ie":"11" }</span></span>
<span class="line"><span>	transform-sticky-regex { "ie":"11" }</span></span>
<span class="line"><span>	transform-unicode-escapes { "ie":"11" }</span></span>
<span class="line"><span>	transform-unicode-regex { "ie":"11" }</span></span>
<span class="line"><span>	transform-spread { "ie":"11" }</span></span>
<span class="line"><span>	transform-destructuring { "ie":"11" }</span></span>
<span class="line"><span>	transform-block-scoping { "ie":"11" }</span></span>
<span class="line"><span>	transform-typeof-symbol { "ie":"11" }</span></span>
<span class="line"><span>	transform-new-target { "ie":"11" }</span></span>
<span class="line"><span>	transform-regenerator { "ie":"11" }</span></span>
<span class="line"><span>	proposal-export-namespace-from { "firefox":"78", "ie":"11", "ios":"12.2", "safari":"13.1" }</span></span>
<span class="line"><span>	syntax-dynamic-import { "android":"86", "chrome":"85", "edge":"86", "firefox":"78", "ie":"11", "ios":"12.2", "opera":"71", "safari":"13.1", "samsung":"12" }</span></span>
<span class="line"><span>	syntax-export-namespace-from { "android":"86", "chrome":"85", "edge":"86", "firefox":"78", "ie":"11", "ios":"12.2", "opera":"71", "safari":"13.1", "samsung":"12" }</span></span>
<span class="line"><span>	syntax-top-level-await { "android":"86", "chrome":"85", "edge":"86", "firefox":"78", "ie":"11", "ios":"12.2", "opera":"71", "safari":"13.1", "samsung":"12" }</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Using polyfills with `usage` option:</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Added following core-js polyfills:</span></span>
<span class="line"><span>	es.array.for-each { "ie":"11" }</span></span>
<span class="line"><span>	web.dom-collections.for-each { "ie":"11" }</span></span></code></pre>
</details>

<p>We are interested in the last three lines, stating that two polyfills were added. The second one, <code>web.dom-collections.for-each</code> is expected as IE 11 does not support <code>.forEach</code> on <code>NodeList</code>. But the first one is peculiar: Why would we polyfill <code>Array.forEach</code>? Based on the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Browser_compatibility">MDN compatibility table</a>, this has been supported since IE 9.</p>
<p>Via <a href="https://github.com/babel/babel/issues/11713#issuecomment-648287888">a comment</a> in a corresponding issue in Babel&#39;s Github repository we can see that <a href="https://github.com/zloirock/core-js/commit/fb6bd7fb4145399cfb8b6add18eb83109686699a">core-js decided to polyfill</a> <code>forEach</code> and similar methods in every IE version (and old versions of other browsers) due to presumably spec-incompliant &quot;early&quot; implementations. Based on the <a href="https://github.com/zloirock/core-js/commit/fb6bd7fb4145399cfb8b6add18eb83109686699a#diff-28c6bedfb82cc6b7bfaab08c2e803bf59681c4654ba0555592030fd34980d06a">code examples</a> I&#39;m bravely assuming that this is irrelevant for my use case. So how to remove this polyfill?</p>
<h2>Optimized output</h2>
<p>Luckily, Babel&#39;s got us covered. <code>preset-env</code> provides an additional <a href="https://babeljs.io/docs/en/babel-preset-env#exclude">exclude</a> option for both Babel plugins and core-js polyfills:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F69D50">exclude</span><span style="color:#ADBAC7">: [</span><span style="color:#96D0FF">"es.array.for-each"</span><span style="color:#ADBAC7">];</span></span></code></pre>
<p>So how big is our remaining output?</p>
<details open>
	<summary>Toggle code</summary>

<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span>!function(){var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function v(r,o,t){if(!function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function")}(r),void 0===o)return r;switch(t){case 0:return function(){return r.call(o)};case 1:return function(t){return r.call(o,t)};case 2:return function(t,e){return r.call(o,t,e)};case 3:return function(t,e,n){return r.call(o,t,e,n)}}return function(){return r.apply(o,arguments)}}function u(t){try{return!!t()}catch(t){return!0}}function e(t){return y.call(t).slice(8,-1)}function L(t){return Object(function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}(t))}function w(t){return 0&#x3C;t?d((t=t,isNaN(t=+t)?0:(0&#x3C;t?S:p)(t)),9007199254740991):0}function o(t){return"object"==typeof t?null!==t:"function"==typeof t}function r(t){if(!o(t))throw TypeError(String(t)+" is not an object");return t}function c(t,e){return V.call(t,e)}function T(t,e){var n;return b(t)&#x26;&#x26;("function"==typeof(n=t.constructor)&#x26;&#x26;(n===Array||b(n.prototype))||o(n)&#x26;&#x26;null===(n=n[H]))&#x26;&#x26;(n=void 0),new(void 0===n?Array:n)(0===e?0:e)}function f(t){throw t}var n,i,l,a,s=(G=function(t){return t&#x26;&#x26;t.Math==Math&#x26;&#x26;t})("object"==typeof globalThis&#x26;&#x26;globalThis)||G("object"==typeof window&#x26;&#x26;window)||G("object"==typeof self&#x26;&#x26;self)||G("object"==typeof t&#x26;&#x26;t)||function(){return this}()||Function("return this")(),y={}.toString,h="".split,E=u(function(){return!Object("z").propertyIsEnumerable(0)})?function(t){return"String"==e(t)?h.call(t,""):Object(t)}:Object,p=Math.ceil,S=Math.floor,d=Math.min,b=Array.isArray||function(t){return"Array"==e(t)},g=!u(function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}),m=s.document,O=o(m)&#x26;&#x26;o(m.createElement),j=!g&#x26;&#x26;!u(function(){return 7!=Object.defineProperty(O?m.createElement("div"):{},"a",{get:function(){return 7}}).a}),M=Object.defineProperty,C={f:g?M:function(t,e,n){if(r(t),e=function(t,e){if(!o(t))return t;var n,r;if(e&#x26;&#x26;"function"==typeof(n=t.toString)&#x26;&#x26;!o(r=n.call(t)))return r;if("function"==typeof(n=t.valueOf)&#x26;&#x26;!o(r=n.call(t)))return r;if(!e&#x26;&#x26;"function"==typeof(n=t.toString)&#x26;&#x26;!o(r=n.call(t)))return r;throw TypeError("Can't convert object to primitive value")}(e,!0),r(n),j)try{return M(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&#x26;&#x26;(t[e]=n.value),t}},A=g?function(t,e,n){return C.f(t,e,{enumerable:!((e=1)&#x26;e),configurable:!(2&#x26;e),writable:!(4&#x26;e),value:n})}:function(t,e,n){return t[e]=n,t},P=s[G="__core-js_shared__"]||function(e,n){try{A(s,e,n)}catch(t){s[e]=n}return n}(G,{}),t=(function(t){(t.exports=function(t,e){return P[t]||(P[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.8.1",mode:"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})}(n={exports:{}}),n.exports),V={}.hasOwnProperty,k=0,x=Math.random(),D=!!Object.getOwnPropertySymbols&#x26;&#x26;!u(function(){return!String(Symbol())}),G=D&#x26;&#x26;!Symbol.sham&#x26;&#x26;"symbol"==typeof Symbol.iterator,N=t("wks"),R=s.Symbol,_=G?R:R&#x26;&#x26;R.withoutSetter||function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++k+x).toString(36)},H=(c(N,n="species")||(D&#x26;&#x26;c(R,n)?N[n]=R[n]:N[n]=_("Symbol."+n)),N[n]),F=[].push,G={forEach:(t=function(y){var h=1==y,p=2==y,S=3==y,d=4==y,b=6==y,g=7==y,m=5==y||b;return function(t,e,n,r){for(var o,i,u=L(t),c=E(u),f=v(e,n,3),l=w(c.length),a=0,r=r||T,s=h?r(t,l):p||g?r(t,0):void 0;a&#x3C;l;a++)if((m||a in c)&#x26;&#x26;(i=f(o=c[a],a,u),y))if(h)s[a]=i;else if(i)switch(y){case 3:return!0;case 5:return o;case 6:return a;case 2:F.call(s,o)}else switch(y){case 4:return!1;case 7:F.call(s,o)}return b?-1:S||d?d:s}})(0),map:t(1),filter:t(2),some:t(3),every:t(4),find:t(5),findIndex:t(6),filterOut:t(7)},I=Object.defineProperty,q={},z=G.forEach,t=!!(l=[]["forEach"])&#x26;&#x26;u(function(){l.call(null,i||function(){throw 1},1)}),G=function(t,e){if(c(q,t))return q[t];var n=[][t],r=!!c(e=e||{},"ACCESSORS")&#x26;&#x26;e.ACCESSORS,o=c(e,0)?e[0]:f,i=c(e,1)?e[1]:void 0;return q[t]=!!n&#x26;&#x26;!u(function(){if(r&#x26;&#x26;!g)return 1;var t={length:-1};r?I(t,1,{enumerable:!0,get:f}):t[1]=1,n.call(t,o,i)})}("forEach"),B=t&#x26;&#x26;G?[].forEach:function(t){return z(this,t,1&#x3C;arguments.length?arguments[1]:void 0)};for(a in{CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}){var J=s[a],K=J&#x26;&#x26;J.prototype;if(K&#x26;&#x26;K.forEach!==B)try{A(K,"forEach",B)}catch(t){K.forEach=B}}document.querySelectorAll("a").forEach(function(t){console.log(t.href)})}();</span></span></code></pre>
</details>

<p>Better, but still pretty big. Turns out even a single polyfill like <code>web.dom-collections.for-each</code> will generate a lot of boilerplate code.</p>
<h2>Further optimizations</h2>
<p>Assuming we care about 4 kB of boilerplate code (coming from the guy serving five tons of JavaScript with his static blog), we can polyfill <code>NodeList.foreach</code> on our own:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F47067">if</span><span style="color:#ADBAC7"> (window.NodeList </span><span style="color:#F47067">&#x26;&#x26;</span><span style="color:#F47067"> !</span><span style="color:#6CB6FF">NodeList</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach) {</span></span>
<span class="line"><span style="color:#6CB6FF">  NodeList</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach </span><span style="color:#F47067">=</span><span style="color:#6CB6FF"> Array</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach;</span></span>
<span class="line"><span style="color:#ADBAC7">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">const</span><span style="color:#6CB6FF"> links</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> document.</span><span style="color:#DCBDFB">querySelectorAll</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"a"</span><span style="color:#ADBAC7">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">links.</span><span style="color:#DCBDFB">forEach</span><span style="color:#ADBAC7">((</span><span style="color:#F69D50">link</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#ADBAC7">  console.</span><span style="color:#DCBDFB">log</span><span style="color:#ADBAC7">(link.href);</span></span>
<span class="line"><span style="color:#ADBAC7">});</span></span></code></pre>
<p>Additionally adding <code>web.dom-collections.for-each</code> to the <code>exclude</code> option will finally give us the following output:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#ADBAC7">(window.NodeList </span><span style="color:#F47067">&#x26;&#x26;</span></span>
<span class="line"><span style="color:#F47067">  !</span><span style="color:#6CB6FF">NodeList</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach </span><span style="color:#F47067">&#x26;&#x26;</span></span>
<span class="line"><span style="color:#ADBAC7">  (</span><span style="color:#6CB6FF">NodeList</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach </span><span style="color:#F47067">=</span><span style="color:#6CB6FF"> Array</span><span style="color:#ADBAC7">.</span><span style="color:#6CB6FF">prototype</span><span style="color:#ADBAC7">.forEach),</span></span>
<span class="line"><span style="color:#ADBAC7">  document.</span><span style="color:#DCBDFB">querySelectorAll</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"a"</span><span style="color:#ADBAC7">).</span><span style="color:#DCBDFB">forEach</span><span style="color:#ADBAC7">(</span><span style="color:#F47067">function</span><span style="color:#ADBAC7"> (</span><span style="color:#F69D50">o</span><span style="color:#ADBAC7">) {</span></span>
<span class="line"><span style="color:#ADBAC7">    console.</span><span style="color:#DCBDFB">log</span><span style="color:#ADBAC7">(o.href);</span></span>
<span class="line"><span style="color:#ADBAC7">  }));</span></span></code></pre>
<p><img src="media/success.png" alt="Glenn Howerton, Rob McElhenney and Charlie Day giving the thumbs up"></p>
<p>However, as we don&#39;t want to go back to manually copying polyfills, we could at least import something like <a href="https://github.com/msn0/mdn-polyfills">mdn-polyfills</a> instead:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F47067">import</span><span style="color:#96D0FF"> "mdn-polyfills/NodeList.prototype.forEach"</span><span style="color:#ADBAC7">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F47067">const</span><span style="color:#6CB6FF"> links</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> document.</span><span style="color:#DCBDFB">querySelectorAll</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"a"</span><span style="color:#ADBAC7">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">links.</span><span style="color:#DCBDFB">forEach</span><span style="color:#ADBAC7">((</span><span style="color:#F69D50">link</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#ADBAC7">  console.</span><span style="color:#DCBDFB">log</span><span style="color:#ADBAC7">(link.href);</span></span>
<span class="line"><span style="color:#ADBAC7">});</span></span></code></pre>
<p>Or we could manually &quot;optimize&quot; our loop:</p>
<pre class="shiki github-dark-dimmed" style="background-color:#22272e;color:#adbac7" tabindex="0"><code><span class="line"><span style="color:#F47067">const</span><span style="color:#6CB6FF"> links</span><span style="color:#F47067"> =</span><span style="color:#ADBAC7"> document.</span><span style="color:#DCBDFB">querySelectorAll</span><span style="color:#ADBAC7">(</span><span style="color:#96D0FF">"a"</span><span style="color:#ADBAC7">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#ADBAC7">[].forEach.</span><span style="color:#DCBDFB">call</span><span style="color:#ADBAC7">(links, (</span><span style="color:#F69D50">link</span><span style="color:#ADBAC7">) </span><span style="color:#F47067">=></span><span style="color:#ADBAC7"> {</span></span>
<span class="line"><span style="color:#ADBAC7">  console.</span><span style="color:#DCBDFB">log</span><span style="color:#ADBAC7">(link.href);</span></span>
<span class="line"><span style="color:#ADBAC7">});</span></span></code></pre>
<p>The latter will reduce code maintainability, though, as removing IE 11 support will take more time than just deleting some <code>import</code>s.</p>
<h2>Takeaways</h2>
<ul>
<li>Babel and core-js are extremely helpful tools and I am very grateful for them.</li>
<li>There are situations where the default output might be bigger than necessary. Spending some time analyzing the generated code allows us to reduce the bundle size.</li>
<li>Using <code>@babel/preset-env</code>&#39;s <code>debug</code> options tells us which polyfills are added. Some polyfills might not be relevant for &quot;standard&quot; use cases as they rather focus on fixing edge cases. These can be removed via the <code>exclude</code> option.</li>
<li>When size is crucial, e.g. when building libraries for consumption by others, manual polyfilling with the help of libraries like <code>mdn-polyfills</code> can be advisable. However, the <code>debug</code> output can still be used to tell us which polyfills we need to add.</li>
</ul>
<br />
<small>
  Photo credits: [FX
  Networks](https://www.fxnetworks.com/shows/its-always-sunny-in-philadelphia)
</small>
]]></description></item><item><title>Controlling our blinds via Siri</title><link>https://blog.responsive.ch/controlling-our-blinds-via-siri/</link><guid isPermaLink="true">https://blog.responsive.ch/controlling-our-blinds-via-siri/</guid><pubDate>Sun, 15 Mar 2020 00:00:00 GMT</pubDate><description><![CDATA[<p>There are two approaches to automating window blinds:</p>
<ul>
<li>Using existing, proven systems like <a href="https://www.somfysystems.com">Somfy</a>.</li>
<li>Vastly overestimating your engineering capabilities and building your own DIY system.</li>
</ul>
<p>Let&#39;s focus on the latter as the former is ridiculous.</p>
<p>There are three main components to this:</p>
<ol>
<li>Remotes to control the whole thing. Usually hand-held ones or special wall switches.</li>
<li>A hub / control center / base station relaying these commands to the receivers. This is optional in case the remotes are capable of interacting with the receivers directly.</li>
<li>Radio controllers receiving these commands to manage the blinds&#39; power supply.</li>
</ol>
<p>Being a devoted member of the Apple cult, using a <a href="https://en.m.wikipedia.org/wiki/HomeKit">Homekit</a>-compatible setup was an obvious choice. This would allow us to use our existing devices instead of having to buy remotes which are a) often limited in how many blinds they can control and b) rather expensive. And not from Apple.</p>
<p>For the radio setup, I decided on the <a href="https://en.wikipedia.org/wiki/Z-Wave">Z-Wave</a> protocol since the ecosystem of both hardware and open-source software seems vibrant. I went out (just kidding, I never left my desk) and bought the following Z-Wave-compatible radio controllers:</p>
<div class="image-table">

<table>
<thead>
<tr>
<th><a href="https://aeotec.com/z-wave-motor-shutter-curtain-control/">Aeotec Nano Shutter</a></th>
<th><a href="https://www.fibaro.com/en/products/smart-roller-shutter/">Fibaro Roller Shutter 3</a></th>
<th><a href="https://qubino.com/products/flush-shutter/">Qubino Flush Shutter</a></th>
</tr>
</thead>
<tbody><tr>
<td><img src="media/aeotec.jpeg" alt="Aeotec Nano Shutter"></td>
<td><img src="media/fibaro.jpeg" alt="Fibaro Roller Shutter 3"></td>
<td><img src="media/qubino.jpeg" alt="Qubino Flush Shutter"></td>
</tr>
</tbody></table>
</div>

<small class="figcaption">
  If you look closely, you can see that the Nano Shutter survived being
  short-circuited for scientific reasons.
</small>

<p>I like the Nano Shutter the best. Mainly because its physical button not only allows you to control Z-Wave inclusion/exclusion (connecting/disconnecting them to the Z-Wave stick), but also to <em>control</em> the blinds. So if somethings goes wrong network-wise, you can still manually close or open the blinds via this button.</p>
<p>To send commands to the controllers, I chose an <a href="https://aeotec.com/z-wave-usb-stick/">Aeotec Z-Stick Gen5</a> and plugged it into my MacBook. Connecting the controllers to the stick uncovered the first issue with the Qubino one: The <a href="http://manuals-backend.z-wave.info/make.php?lang=en&sku=GOAEZMNHCD1">manual</a> told me to either <code>Press push button I1</code> (which would have required me to connect an external, physical button to this input) or <code>Press service button S (only applicable for 24 V SELV supply voltage)</code> (which is a physical button on the controller, however, I was using 220 V). A nice fellow in a <a href="https://www.domoticz.com/forum/viewtopic.php?t=25217">home automation forum</a> convinced me to use the latter anyway: <code>In fact 220V also works but you have a risk of electro shock while pressing that stupid S button</code>.</p>
<p>Still alive and optimistic, I opened my code editor. Being fluent in the only reasonable programming language there is, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript">JavaScript</a>, my first attempt was using <a href="https://homebridge.io">Homebridge</a>. I went through every Homebridge-Z-Wave implementation I could find but struggled to consistently send reasonable commands to the controllers. Most of the time, they showed up as light bulbs in Apple&#39;s <a href="https://www.apple.com/ios/home/">Home app</a>. Which is not completely unreasonable, but still not ideal.</p>
<p>At some point, I decided to have a look at <a href="https://www.home-assistant.io">Home Assistant</a>, a very well-documented open-source home automation solution. While I was <a href="https://github.com/home-assistant/home-assistant.io/pull/12377">unsuccessful with the Docker setup</a> and had to jump through <a href="https://stackoverflow.com/questions/42098126/mac-osx-python-ssl-sslerror-ssl-certificate-verify-failed-certificate-verify">some hoops</a> to run it natively, I was able to connect both the USB stick and the controllers and they properly showed up as window blinds in my Home app. 🎉</p>
<p>After my wife showed a complete lack of understanding for the fact that the blinds would only work if my laptop was up and running, I ordered my first <a href="https://www.raspberrypi.org">Raspberry Pi</a>. This was as much fun as anticipated, definitely recommended. The only thing not immediately obvious to me was that I had to reflash the SD card bundled with <a href="https://www.digitec.ch/en/s1/product/raspberry-pi-4-4g-model-b-full-starter-kit-armv8-development-boards-kits-11764848">my starter kit</a> with <a href="https://www.raspberrypi.org/downloads/raspbian/">Raspbian</a> since the default installer apparently requires <a href="https://www.raspberrypi.org/forums/viewtopic.php?t=172862">connecting a screen and keyboard</a> to the Pi (which I refused).</p>
<p>Everything else was smooth as an android&#39;s bottom. That is until I wasn&#39;t able to connect the Z-Wave UBS stick. The corresponding <a href="https://github.com/raspberrypi/linux/issues/3027">Github issue</a> alternatingly blamed Raspberry and Aeotec and stated that the problem was both definitely solved and definitely not solved with every existing USB hub stuck between the Pi and the stick. Luckily, the first cheap hub I found worked out just fine.</p>
<figure>

<p><img src="media/raspberry.jpeg" alt="Raspberry"></p>
<figcaption>

<p>Z-Wave stick connected to Raspberry Pi via simple HAMA USB hub.</p>
</figcaption>

</figure>

<p>The only thing left was configuring Home Assistant. Which was a great experience. A very intuitive UI (which I can&#39;t say for paid alternatives like <a href="https://www.indigodomo.com/">this one</a>) and, again, really well-documented. After discovering that the Nano Shutter did not consistently show up in Home after restarting the Pi, a quick search through their Github issues relevaled that <a href="https://github.com/home-assistant/core/issues/20032">delaying the start of the Homekit integration</a> solved the issue.</p>
<p>So this is my life now:</p>
<blockquote>
<p>Hey Siri, please open the blinds. I want to check the weather to see which pair of sweatpants I should wear for lunch.</p>
</blockquote>
]]></description></item></channel></rss>