<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Henrik Sommerfeld</title>
    <description>The latest articles on DEV Community by Henrik Sommerfeld (@p4lm).</description>
    <link>https://dev.to/p4lm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F29124%2F3158d5eb-280c-434b-afb7-c0a3822f9160.jpg</url>
      <title>DEV Community: Henrik Sommerfeld</title>
      <link>https://dev.to/p4lm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/p4lm"/>
    <language>en</language>
    <item>
      <title>Usability Learnings from Building a CLI</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Tue, 17 Nov 2020 21:36:00 +0000</pubDate>
      <link>https://dev.to/p4lm/usability-learnings-from-building-a-cli-34pc</link>
      <guid>https://dev.to/p4lm/usability-learnings-from-building-a-cli-34pc</guid>
      <description>&lt;p&gt;From the past months of iterating on a CLI for managing micro services in our company, I've drawn some conclusions regarding usability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;As the number of commands, arguments and flags have grown, so has the risk of confusion. Naming things is not the easiest thing in our industry – should the command for creating a service be called &lt;strong&gt;create&lt;/strong&gt;, &lt;strong&gt;init&lt;/strong&gt; or &lt;strong&gt;add&lt;/strong&gt;? Since it's &lt;a href="https://en.wikipedia.org/wiki/Idempotence"&gt;idempotent&lt;/a&gt; (can be run multiple times), should it instead be called &lt;strong&gt;ensure&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;After changing this back and forth, to ensure/create/add confusion, we sort of gave up and used aliases. &lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;We use &lt;a href="https://oclif.io/"&gt;Oclif&lt;/a&gt; (The Open CLI Framework), which provides a few features we can use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aliases
&lt;/h3&gt;

&lt;p&gt;By using aliases, you don't have to think too hard on whether it's supposed to be get/set, show/edit or list/add/remove.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;UpstreamsShow&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;aliases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upstreams:get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upstreams:list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Show service upstreams&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upstreams:show&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;examples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;`$ aw upstreams:show -s hello-cats -e dev
Lists the upstream services for service 'hello-cats' in environment 'dev'.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`$ aw upstreams:show
Lists the upstream services for current service (from repo). You will be prompted for environment.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Help
&lt;/h3&gt;

&lt;p&gt;By making sure all commands have a description, usage and possibly examples, &lt;code&gt;--help&lt;/code&gt; becomes better. Examples are really good for commands with several arguments or flags. When using topics in Oclif, it's also helpful to add command descriptions in package.json as described on the &lt;a href="https://oclif.io/docs/topics"&gt;topics documentation page&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Autocomplete
&lt;/h3&gt;

&lt;p&gt;I also find &lt;a href="https://github.com/oclif/plugin-autocomplete"&gt;the autocomplete plugin&lt;/a&gt; helpful, since you can just tab your way to the full command.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/HgmbouS1ynw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>ux</category>
      <category>cli</category>
      <category>devops</category>
    </item>
    <item>
      <title>Search for Static Website Without External Service</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Thu, 23 Jul 2020 21:28:40 +0000</pubDate>
      <link>https://dev.to/p4lm/search-for-static-website-without-external-service-3c7m</link>
      <guid>https://dev.to/p4lm/search-for-static-website-without-external-service-3c7m</guid>
      <description>&lt;p&gt;When you have a static website, there are a few things that you usually don't have out-of-the-box. One such thing is &lt;em&gt;search&lt;/em&gt;. You can argue that you don't need it, but if you want it and your site isn't that large, I'll describe how I've set it up without an external service. &lt;/p&gt;

&lt;p&gt;This post is part 5 of my &lt;a href="https://dev.to/p4lm/hugo-pipeline-series-intro-5gf4"&gt;Hugo Pipeline Series&lt;/a&gt;, so I'll use Hugo as the example here, but I've done a similar setup with &lt;a href="https://www.gatsbyjs.org/packages/@gatsby-contrib/gatsby-plugin-elasticlunr-search/?=lunr"&gt;this Gatsby plugin&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;The steps I use are the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a json file with everything I want in my search index (Hugo)&lt;/li&gt;
&lt;li&gt;Create a search index from the json file (NodeJS)&lt;/li&gt;
&lt;li&gt;Download and load the index (Web Browser)&lt;/li&gt;
&lt;li&gt;Perform search and present results (Web Browser)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Create file to index
&lt;/h2&gt;

&lt;p&gt;I have a Hugo layout for the indexable content where I output all pages of the types I want. The type &lt;strong&gt;article&lt;/strong&gt; is what all blog posts use and &lt;strong&gt;shortcuts-windows7&lt;/strong&gt; is a special layout I want to include in search (&lt;a href="https://www.henriksommerfeld.se/kortkommandon-windows7/"&gt;see it here&lt;/a&gt;, if you're curious). &lt;a href="https://www.henriksommerfeld.se/about/"&gt;My About page&lt;/a&gt; is not included, since I figure you can find that anyway if you can find the search feature.&lt;/p&gt;

&lt;p&gt;Title, relative permalink, tags, the full content as plain text, the summary (excerpt) and the date (formatted and raw), are the fields I picked as searchable + available for search result presentation.&lt;/p&gt;

&lt;p&gt;I also exclude the list page named &lt;em&gt;Articles&lt;/em&gt; (that I don't know how to get rid of, please &lt;a href="https://github.com/henriksommerfeld/blog-hugo"&gt;create a PR&lt;/a&gt; if you know &lt;em&gt;how&lt;/em&gt; and want to help). &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/henriksommerfeld/blog-hugo/blob/master/layouts/search-index/single.html"&gt;&lt;code&gt;layouts/search-index/single.html&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{- $.Scratch.Add "index" slice -}}
{{- range where .Site.Pages "Type" "in" (slice "article" "shortcuts-windows7") -}}
    {{- if ne .Title "Articles" -}}
        {{- $.Scratch.Add "index" (dict "title" .Title "ref" .RelPermalink "tags" .Params.tags "content" .Plain "summary" (partial "summary.html" .) "dateformatted" (dateFormat "2, January 2006" .Date) "dateiso" (time .Date)) -}}
    {{- end -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This layout needs to be referenced and for that I have &lt;a href="https://github.com/henriksommerfeld/blog-hugo/blob/master/content/search-index.md"&gt;&lt;code&gt;search-index.md&lt;/code&gt;&lt;/a&gt; which is empty, except for the frontmatter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="err"&gt;---&lt;/span&gt;
&lt;span class="err"&gt;date:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="mi"&gt;2017-06-21&lt;/span&gt;&lt;span class="err"&gt;T&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;span class="err"&gt;title:&lt;/span&gt; &lt;span class="err"&gt;"search&lt;/span&gt; &lt;span class="err"&gt;index"&lt;/span&gt;
&lt;span class="err"&gt;type:&lt;/span&gt; &lt;span class="err"&gt;"search-index"&lt;/span&gt;
&lt;span class="err"&gt;url:&lt;/span&gt; &lt;span class="err"&gt;"data-to-index.json"&lt;/span&gt;
&lt;span class="err"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Create index
&lt;/h2&gt;

&lt;p&gt;Now that we have something to index, it's time to switch to NodeJS land and install &lt;a href="https://lunrjs.com/"&gt;Lunr&lt;/a&gt;, &lt;code&gt;yarn add lunr&lt;/code&gt;. I have a script that reads the file created in the previous step (&lt;a href="https://www.henriksommerfeld.se/data-to-index.json"&gt;&lt;code&gt;data-to-index.json&lt;/code&gt;&lt;/a&gt;) and creates a new file, &lt;a href="https://www.henriksommerfeld.se/search-index.json"&gt;&lt;code&gt;search-index.json&lt;/code&gt;&lt;/a&gt; in the output directory (&lt;code&gt;public&lt;/code&gt;). This is also the place to configure Lunr with &lt;em&gt;boosting&lt;/em&gt; and such. I'm not good att tweaking search, so these settings are pretty basic. This was written before I got more heavily into NodeJS development, but it has worked without problems for a few years now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lunr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lunr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputFilePathParameter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../public/data-to-index.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="nx"&gt;outputFilePathParameter&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../public/search-index.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reading &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;inputFilePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;documentsToIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputFilePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Indexing &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;inputFilePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lunr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ref&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;documentsToIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dateiso&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dateiso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dateformatted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dateformatted&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Saving index at &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;outputFilePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataToSave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputFilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENOENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flag&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputFilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataToSave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Saved index at &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;outputFilePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This is run with an npm script &lt;em&gt;after&lt;/em&gt; Hugo has produced its output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; node build/index-search.js public/search-index.json

Reading /Users/henrik/Code/blog-hugo/public/data-to-index.json
Indexing /Users/henrik/Code/blog-hugo/public/data-to-index.json
Saving index at public/search-index.json
Saved index at public/search-index.json
✨ Done &lt;span class="k"&gt;in &lt;/span&gt;0.52s.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;To have the search index available during development, I run the Hugo command twice. This isn't perfect, but since &lt;code&gt;hugo server&lt;/code&gt; (like most dev servers) doesn't save the files on disk, this is necessary and not really a problem. The npm script looks like this: &lt;code&gt;hugo &amp;amp;&amp;amp; npm run index &amp;amp;&amp;amp; npm run hugo:watch&lt;/code&gt; (see &lt;a href="https://github.com/henriksommerfeld/blog-hugo/blob/master/package.json"&gt;full package.json here&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Loading index
&lt;/h2&gt;

&lt;p&gt;Most of my visitors come straight to a post from a Google search, so I'm probably the biggest user of the site search myself (maybe the only one 😳). Therefor I don't want the search index to be downloaded before the user has shown an intention to use the search feature. The index is currently a download of 134 kB (compressed), which I think is fine considering that people are watching video on web pages and that the alternative of using an external service has several other drawbacks (complexity, cost, etc). Still, the index size is worth keeping an eye on and this setup requires error handling (if the download fails or the user has started to type before the download is complete).&lt;/p&gt;

&lt;p&gt;The index are downloaded through a regular &lt;code&gt;fetch&lt;/code&gt; call when the search dialog is opened (the &lt;code&gt;open&lt;/code&gt; function).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;textInSearchBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;indexLoadFailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;indexLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isModalOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textInSearchBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexLoadFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;downloadIndex&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;downloadIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetchIndex&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lunr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchBoxChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textInSearchBox&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🔍 Search index downloaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;fetchIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search-index.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleFetchResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleFetchResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;handleFetchResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexLoadFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Searching and presenting results
&lt;/h2&gt;

&lt;p&gt;I have touched on this in my previous &lt;a href="https://dev.to/p4lm/alpine-js-benefits-and-limitations-hho#an-example-with-search-results"&gt;post about Alpine.js&lt;/a&gt;, so go there for more code, but this is simply about calling the &lt;code&gt;search&lt;/code&gt; function on the Lunr index. Since everything is in memory, I call the search function on every keypress in the searchbox. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--z-fpyH2s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/c6gyemc289zf4onyomqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--z-fpyH2s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/c6gyemc289zf4onyomqt.png" alt="Screenshot of search results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Good luck in implementing your own site search!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>hugo</category>
      <category>lunr</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Alpine.js – Benefits and Limitations</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Mon, 29 Jun 2020 20:14:54 +0000</pubDate>
      <link>https://dev.to/p4lm/alpine-js-benefits-and-limitations-hho</link>
      <guid>https://dev.to/p4lm/alpine-js-benefits-and-limitations-hho</guid>
      <description>&lt;p&gt;This post is part of my &lt;a href="https://dev.to/p4lm/hugo-pipeline-series-intro-5gf4"&gt;Hugo Pipeline Series&lt;/a&gt;, but the benefits and limitations I discuss are not specific to using &lt;a href="https://github.com/alpinejs/alpine"&gt;Alpine.js&lt;/a&gt; together with &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  What is Alpine.js and why?
&lt;/h2&gt;

&lt;p&gt;Alpine.js is meant to be used with an existing HTML document (server-side rendered HTML that isn't produced by a JavaScript framework), just like plain JavaScript or &lt;a href="https://jquery.com/"&gt;jQuery&lt;/a&gt;. This is the way it was done when JavaScript was introduced and how it's done today when using Hugo.&lt;/p&gt;

&lt;p&gt;I use JavaScript for the following features on my blog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opening and closing a hamburger menu&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/p4lm/lazy-loading-images-in-hugo-45e0"&gt;Lazy loading of images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Lightbox for images and code&lt;/li&gt;
&lt;li&gt;Providing site search&lt;/li&gt;
&lt;li&gt;Easter eggs&lt;/li&gt;
&lt;li&gt;Comments and analytics (but that's not my code)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I started out using jQuery when migrating the blog &lt;a href="https://www.henriksommerfeld.se/switching-from-wordpress-to-hugo"&gt;from WordPress to Hugo&lt;/a&gt;, which I think was the obvious choice at the time. Later I migrated to plain JavaScript. That was fairly straight forward and the code looked quite similar after the migration, although a bit lengthier. This worked fine and I didn't need a library at all, so why add one again?&lt;/p&gt;

&lt;p&gt;Looking at what the JavaScript I have is doing, we can see where I can benefit from using a library:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing CSS classes on an element, mostly &lt;code&gt;body&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adding event listeners to handle interactions&lt;/li&gt;
&lt;li&gt;Rendering search results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all these areas I benefit from using declarative code, it's just less code and easier to read. This is where Alpine.js comes in. Alpine.js has borrowed a lot from &lt;a href="https://vuejs.org/"&gt;Vue.js&lt;/a&gt; when it comes to syntax, but work with an existing &lt;a href="https://en.wikipedia.org/wiki/Document_Object_Model"&gt;DOM&lt;/a&gt;. I haven't used Vue.js and that can make you feed a bit excluded when the documentation explains something by saying that &lt;em&gt;"it works just like in Vue.js"&lt;/em&gt;.  It is however a small API, so I found it easy to get started with.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example with keyboard navigation
&lt;/h2&gt;

&lt;p&gt;This is the relevant code I use for showing/hiding &lt;code&gt;outline&lt;/code&gt; for the element that has focus, based on whether the user is navigating by mouse or keyboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"window.blog"&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"{ 'keyboard-navigation' : keyboardNavigation }"&lt;/span&gt;
  &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;mouseup=&lt;/span&gt;&lt;span class="s"&gt;"keyboardNavigation = false"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;keydown.tab=&lt;/span&gt;&lt;span class="s"&gt;"keyboardNavigation = true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
…
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keyboardNavigation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  CSS
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="nc"&gt;.keyboard-navigation&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Doing this with imperative code is simply messier, so this is one example where Alpine.js helps. &lt;/p&gt;

&lt;h2&gt;
  
  
  An example with search results
&lt;/h2&gt;

&lt;p&gt;Another example is the search results I present. This is a situation where I could just add any of the popular JavaScript frameworks, since this part of the page creates the HTML in JavaScript. This is also a situation where pure JavaScript quickly gets messy, like concatenating strings and setting &lt;code&gt;innerHTML&lt;/code&gt;, especially if you need event listeners on those new elements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"search-results-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"search-output"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"search.textInSearchBox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"no-results-message"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"search.store &amp;amp;&amp;amp; search.textInSearchBox &amp;amp;&amp;amp; !search.hits.length"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      No matching posts found. You can use wildcards and search only in titles, e.g. &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;title:iot&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"index-loading-message"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"!search.indexLoadFailed &amp;amp;&amp;amp; search.indexLoading &amp;amp;&amp;amp; search.textInSearchBox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"icon-spinner"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt; Loading search index, please wait...
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"index-failed-message"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"search.indexLoadFailed &amp;amp;&amp;amp; search.textInSearchBox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Search index failed to download 😢
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"number-of-hits-message"&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"search.getHitsText()"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"search.hits.length"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"result-list"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"search.hits.length"&lt;/span&gt; &lt;span class="na"&gt;x-ref=&lt;/span&gt;&lt;span class="s"&gt;"hits"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;x-for=&lt;/span&gt;&lt;span class="s"&gt;"hit in search.hits"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"hit.ref"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;:href=&lt;/span&gt;&lt;span class="s"&gt;'hit.ref'&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"search.fromStore(hit).title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"entry-meta"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;time&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"published"&lt;/span&gt; &lt;span class="na"&gt;:datetime=&lt;/span&gt;&lt;span class="s"&gt;"search.fromStore(hit).dateiso"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"icon icon-calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;xlink:href=&lt;/span&gt;&lt;span class="s"&gt;"#icon-calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/use&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"search.fromStore(hit).dateformatted"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/time&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"search.fromStore(hit).summary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;search&lt;/code&gt; is the object that contains the functions and properties being referenced in this mark-up. It's in a separate JavaScript file not included here, but hopefully you get the point of the declarative approach instead of doing this in imperative JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;p&gt;Hopefully I've been able to highlight some of the benefits in the examples above, but to conclude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to get started with&lt;/li&gt;
&lt;li&gt;Same kind of declarative data binding that we love with other JavaScript frameworks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Now to the interesting stuff, things that Alpine.js &lt;em&gt;not&lt;/em&gt; so good for – the stuff you generally don't find in documentation or tutorials.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You cannot have nested components or have them communicate easily.&lt;/li&gt;
&lt;li&gt;The page (DOM) isn't updated when updates are triggered by non-interactive events.&lt;/li&gt;
&lt;li&gt;Doesn't work with Turbolinks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the case of my blog, I made the &lt;code&gt;body&lt;/code&gt; tag the Alpine component, which works just fine as I'm mostly setting different CSS classes on the body tag anyway. For a more complex use, &lt;a href="https://codewithhugo.com/alpinejs-component-communication-event-bus/"&gt;A guide to Alpine.js component communication&lt;/a&gt; describes how you can have sibling components talk to each other and have the DOM react to non-interactive events, &lt;a href="https://github.com/alpinejs/alpine/discussions/585"&gt;see answer on GitHub&lt;/a&gt;. A non-interactive event is when the user hasn't clicked or typed anything, such as when data is fetched (a promise is resolved) and you set that data to a property. &lt;/p&gt;

&lt;p&gt;The theme switcher I have reacts to theme (light/dark) changes in the operating system and also when the theme setting in &lt;code&gt;localStorage&lt;/code&gt; is changed. The code I have to listen for those events can update a property bound to the Alpine component, but it won't update the DOM. Rather than implementing some involved dispatch mechanism, I prefer to use pure DOM manipulation for these situation, starting with &lt;code&gt;document.getElementById()&lt;/code&gt; and setting the element's properties.&lt;/p&gt;

&lt;p&gt;Another thing to note is that if you're using &lt;a href="https://github.com/turbolinks/turbolinks"&gt;Turbolinks&lt;/a&gt; (which gives navigation without full page reloads), &lt;a href="https://github.com/alpinejs/alpine/issues/319"&gt;it doesn't seem to work with Alpine.js&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Overall, I think the migration from plain JavaScript to Alpine.js was worth it for me. The code is easier to read now and that's what I was aiming for. I just wish I understood the limitations earlier, that would have saved some time.&lt;/p&gt;

&lt;p&gt;There are also features of Alpine.js that I don't use, namely animations and &lt;code&gt;x-ref&lt;/code&gt; when using a .js file. Maybe this is because I came from a world of plain JavaScript and animations in CSS. It seems that the convention when using Alpine.js is to include all JavaScript in &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags rather than separate .js files. I didn't go that route and found that &lt;code&gt;document.getElementById()&lt;/code&gt; works just as well as passing x-refs around (they don't seem to work in .js files otherwise). Using a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag is probably better as long as the code is as simple as in my keyboard navigation example above, but as it grows, I find it better to use a separate file 🤷‍♂️&lt;/p&gt;

</description>
      <category>alpinejs</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>hugo</category>
    </item>
    <item>
      <title>Hugo Pipeline Series – Developing and Deploying</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Mon, 22 Jun 2020 13:53:06 +0000</pubDate>
      <link>https://dev.to/p4lm/hugo-pipeline-series-developing-and-deploying-332c</link>
      <guid>https://dev.to/p4lm/hugo-pipeline-series-developing-and-deploying-332c</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DRLAYTCE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/m8wktuqvr532x1yiye7p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DRLAYTCE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/m8wktuqvr532x1yiye7p.png" alt="Pull request workflow using GitHub's CLI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In terms of making code changes to my Hugo site, I'll focus on the JavaScript parts, since Hugo templates and CSS isn't much to talk about. I use a few libraries that I've installed with &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt; and those need to be processed before they are sent to the browser. The JavaScript code I have written myself, does not have that requirement. In that case it's just a matter of how old browsers I want to support. &lt;/p&gt;

&lt;p&gt;So, by splitting libraries (installed through npm) from my own code, I'm able to rely solely on Hugo's file watcher with live reload. For development (using &lt;code&gt;hugo server&lt;/code&gt;), the libraries are built once, and the file with custom code is served to the browser as is. I use &lt;a href="http://browserify.org/"&gt;Browserify&lt;/a&gt; for the libraries and that's good enough for my needs. If you need something more powerful, you might consider &lt;a href="https://github.com/netlify-templates/victor-hugo"&gt;Victor Hugo&lt;/a&gt; that comes with &lt;a href="https://webpack.js.org/"&gt;Webpack&lt;/a&gt; preconfigured. I could also have referenced the libraries directly from a public &lt;a href="https://en.wikipedia.org/wiki/Content_delivery_network"&gt;CDN&lt;/a&gt;, but that would make it harder to see when there's a new version of a library and more importantly – my site wouldn't work on localhost without an Internet connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Site&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinifyBundles&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt; &lt;span class="s"&gt;"minified"&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="s"&gt;"compact"&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="s"&gt;"noComments"&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt; &lt;span class="s"&gt;"main.js"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;babel&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt; &lt;span class="s"&gt;"libraries.js"&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;slice&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Concat&lt;/span&gt; &lt;span class="s"&gt;"bundle.js"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fingerprint&lt;/span&gt; &lt;span class="s"&gt;"sha512"&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $bundle.RelPermalink }}"&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $bundle.Data.Integrity }}"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt; &lt;span class="s"&gt;"libraries.js"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fingerprint&lt;/span&gt; &lt;span class="s"&gt;"sha512"&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $libs.RelPermalink }}"&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $libs.Data.Integrity }}"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mainDev&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt; &lt;span class="s"&gt;"main.js"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fingerprint&lt;/span&gt; &lt;span class="s"&gt;"sha512"&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $mainDev.RelPermalink }}"&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $mainDev.Data.Integrity }}"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For the "production build", I just minify the code and concatenate the libraries with my custom code into a single file. I use &lt;a href="https://babeljs.io/"&gt;Babel&lt;/a&gt; from within the Hugo template (&lt;a href="https://gohugo.io/hugo-pipes/babel/"&gt;available since Hugo 0.70&lt;/a&gt;) which removes the need for yet another npm script.&lt;/p&gt;

&lt;p&gt;When doing code changes I always create a new branch in Git. As I &lt;a href="https://dev.to/p4lm/hugo-pipeline-series-editing-and-deploying-1cbm"&gt;described in the previous post&lt;/a&gt;, anything pushed to master is automatically built and deployed (if build is successful) to the live site without any tests. By creating a pull request and using &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt;, I can feel confident that I haven't broken anything when the checks are green. &lt;/p&gt;

&lt;p&gt;These are the steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a feature branch in Git&lt;/li&gt;
&lt;li&gt;Develop the new feature in the new branch&lt;/li&gt;
&lt;li&gt;Create a &lt;em&gt;pull request&lt;/em&gt; from the feature branch to &lt;em&gt;master&lt;/em&gt; branch (through GitHub)&lt;/li&gt;
&lt;li&gt;Netlify builds &amp;amp; deploys a canary release on a separate URL (this is reported back to the PR in GitHub&lt;/li&gt;
&lt;li&gt;When the canary release URL responds with a 200 status code, Cypress tests are run in GitHub Actions&lt;/li&gt;
&lt;li&gt;Test results are shown in the PR in GitHub. If green, I merge the feature branch into master&lt;/li&gt;
&lt;li&gt;Netlify deploys the latest code from the master branch to
the live site (Prod) &lt;/li&gt;
&lt;li&gt;GitHub Actions workflow waits 2 min and then performs a Lighthouse audit against the live site&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When I create a pull request, either through GitHub's web interface or preferably by using their CLI (&lt;code&gt;gh pr create&lt;/code&gt;), two things happen. (1) Netlify deploys a preview of the site (canary release) and (2) GitHub Actions runs my &lt;a href="//cypress.io/"&gt;Cypress&lt;/a&gt; tests against that deployed site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FhJ0SHsE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/iackwtv3rh0p174e8xsx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FhJ0SHsE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/iackwtv3rh0p174e8xsx.png" alt="All checked passed in GitHub pull request"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running the tests against a deployed site (as opposed to a dev server on localhost) transform the tests from function tests to end-to-end tests. Before I had this setup I once broke the &lt;em&gt;comments&lt;/em&gt; feature (using &lt;a href="https://disqus.com/"&gt;Disqus&lt;/a&gt;) by fiddling with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP"&gt;Content Security Policy&lt;/a&gt; headers, something I would have caught today when running the tests against a deployed site.&lt;/p&gt;

&lt;p&gt;Since both building + deploying a site and installing Cypress to run the tests take some time, it's great that it can run in parallel. The &lt;code&gt;yarn install&lt;/code&gt; run in GitHub Actions is typically done before the canary release is deployed by Netlify, so I use an action that waits for a 200 response from Netlify and then run the tests. Super happy with this setup!&lt;/p&gt;

&lt;h3&gt;
  
  
  master-pull-request.yml
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;yarn install&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Waiting for 200 from the Netlify Preview&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jakepartusch/wait-for-netlify-action@v1.1&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;waitFor200&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;site_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;henriksommerfeld"&lt;/span&gt;
          &lt;span class="na"&gt;max_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Integration tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;CYPRESS_BASE_URL=${{ steps.waitFor200.outputs.url }} npm run cypress:cli     &lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Lighthouse&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;foo-software/lighthouse-check-action@master&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lighthouseCheck&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LIGHTHOUSE_CHECK_GITHUB_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;          
          &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref }}&lt;/span&gt;
          &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.waitFor200.outputs.url }}&lt;/span&gt;
          &lt;span class="na"&gt;sha&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;prCommentEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;slackWebhookUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LIGHTHOUSE_CHECK_WEBHOOK_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;As you can see from my workflow file above, I get a notification in &lt;a href="https://slack.com"&gt;Slack&lt;/a&gt; (I have a personal workspace) when the canary release is deployed and when a &lt;a href="https://developers.google.com/web/tools/lighthouse"&gt;Lighthouse&lt;/a&gt; audit is completed (after the Cypress tests). If I've made changes to the look and feel (like a CSS change), I naturally take a look at the deployed canary release, but if not, I'll just go ahead and merge (which triggers a deploy to the live site).&lt;/p&gt;

&lt;p&gt;One thing to say about the Lighthouse audits is that they are of questionable value for the canary releases. The performance score is always lower on the first page load, so you need to load at least two pages to get a fair result. Secondly, the SEO score won't say much since the canary release is purpously blocked from indexing and has a different URL than the canonical URL set for the site. For this reason, I run a Lighthouse audit on the live site as well. In this case I can't wait for a 200 response, so I'll just wait a bit longer than a deploy normally takes and run the audit after that.&lt;/p&gt;

&lt;h3&gt;
  
  
  master-push.yml
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait 2 min for Netlify Deploy to complete&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jakejarvis/wait-action@master&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2m'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hit once&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wei/curl@master&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.henriksommerfeld.se&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Lighthouse&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;foo-software/lighthouse-check-action@master&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lighthouseCheck&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LIGHTHOUSE_CHECK_GITHUB_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;          
          &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref }}&lt;/span&gt;
          &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://www.henriksommerfeld.se/about,https://www.henriksommerfeld.se'&lt;/span&gt;
          &lt;span class="na"&gt;sha&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;prCommentEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;slackWebhookUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LIGHTHOUSE_CHECK_WEBHOOK_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The Slack notifications from a "production deploy" looks something like this:&lt;/p&gt;

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

</description>
      <category>hugo</category>
      <category>webdev</category>
      <category>blog</category>
    </item>
    <item>
      <title>Hugo Pipeline Series – Editing and Deploying</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Mon, 15 Jun 2020 07:09:47 +0000</pubDate>
      <link>https://dev.to/p4lm/hugo-pipeline-series-editing-and-deploying-1cbm</link>
      <guid>https://dev.to/p4lm/hugo-pipeline-series-editing-and-deploying-1cbm</guid>
      <description>&lt;p&gt;In the first post in this series I said that I don't use a proper Content Management System (CMS). To manage my content I use a code editor (&lt;a href="https://code.visualstudio.com/"&gt;VS Code&lt;/a&gt;) and &lt;a href="https://en.wikipedia.org/wiki/Git"&gt;Git&lt;/a&gt; (&lt;a href="https://github.com/"&gt;GitHub&lt;/a&gt;). The advantage of having my content in text files in the same repository as the code is huge. No database to backup or sync between environments.&lt;/p&gt;

&lt;p&gt;See &lt;a href="http://www.youtube.com/watch?v=7_sXnMevPH4&amp;amp;t=5m25s"&gt;Scott Hanselman looking at all his blog posts&lt;/a&gt; from 2005 and onwards. It's all XML files, which might not be trendy today, but is still human readable and easily convertible to another text format. I use &lt;a href="https://en.wikipedia.org/wiki/Markdown"&gt;Markdown&lt;/a&gt; now, but the important thing is that it's text files that can be converted if I need to. I have already changed the code highlighting once, which was an easy search-and-replace operation. When I did the same in &lt;a href="https://wordpress.org/"&gt;WordPress&lt;/a&gt; some years ago, it was…harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different workflows for content and code changes
&lt;/h2&gt;

&lt;p&gt;Since correcting a simple spelling mistake using a static site generator requires a new build, it's beneficial to reduce the amount of stuff that needs to happen between a push and a deploy. Therefore I have split content changes and code changes (see next post) into two different workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reduce the stuff to build
&lt;/h3&gt;

&lt;p&gt;I have some npm packages and have split &lt;code&gt;dependencies&lt;/code&gt; from &lt;code&gt;devDependencies&lt;/code&gt; to reduce the amount of packages that need to be installed for a content change. Installing dependencies with &lt;code&gt;yarn install --production&lt;/code&gt; installs 44 MB of &lt;em&gt;node_modules&lt;/em&gt;, while &lt;code&gt;yarn install&lt;/code&gt; installs 110 MB of &lt;em&gt;node_modules&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;My netlify.toml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[build.environment]&lt;/span&gt;
  &lt;span class="py"&gt;HUGO_VERSION&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.72.0"&lt;/span&gt;
  &lt;span class="py"&gt;YARN_VERSION&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.22.4"&lt;/span&gt;
  &lt;span class="py"&gt;YARN_FLAGS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"--production"&lt;/span&gt;
  &lt;span class="py"&gt;NODE_ENV&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"production"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;My content deployment workflow consists of pushing directly to the master branch. That triggers a web hook that does a build and deploy by &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt;. This requires the discipline not to push code changes directly to the master branch, but since I'm the only developer, that's a policy easy to enforce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--y5Ow4lzB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h4slmihjtrooxf31sqgq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--y5Ow4lzB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h4slmihjtrooxf31sqgq.png" alt="Deploy pipeline with VS Code, GitHub and Netlify"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Images
&lt;/h3&gt;

&lt;p&gt;The biggest speed difference, both in building on my own laptop and in deploying, is how images are handled. This is strikingly obvious when I compare to the two &lt;a href="https://www.gatsbyjs.com/"&gt;Gatsby&lt;/a&gt; sites I have. When writing or editing a blog post, I have the &lt;a href="https://gohugo.io/commands/hugo_server/"&gt;Hugo development server&lt;/a&gt; running. When I add a new image, Hugo creates the different versions (sizes) of the image that my templates specify – once. Then I commit the Markdown file and the images (original and generated) to Git. That's it, the image processing for that specific image is done, it won't ever have to be processed again unless I want to. No CPU cycles are spent on any server to generate images and that saves time in deployment.  It also saves time against the free build minutes quota I have, currently 300 minutes per month on Netlify. &lt;/p&gt;

&lt;p&gt;The local dev server never have to re-generate images it has already processed, so it starts quicker as well. Just as a test when I created a new shortcode with new image sizes, I ran Hugo with the option to re-generate everything from scratch (Markdown, SCSS and images), &lt;code&gt;hugo --ignoreCache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   | EN   
-------------------+------
  Pages            | 197  
  Paginator pages  |  42  
  Non-page files   |  97  
  Static files     |  27  
  Processed images | 459  
  Aliases          |  63  
  Sitemaps         |   1  
  Cleaned          |   0 

Total in 39741 ms
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;All of the 8 cores on my laptop where working here and it took 40 seconds. This is something I won't most likely ever have to do again, so 40 &lt;em&gt;minutes&lt;/em&gt; would have been fine in this extreme situation as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Total build &amp;amp; deploy time
&lt;/h2&gt;

&lt;p&gt;As I mentioned, I use Netlify to both build and deploy. That process &lt;a href="https://app.netlify.com/sites/henriksommerfeld/deploys/5edd46691261090008d5a8b5"&gt;takes about 2 minutes&lt;/a&gt;, which regardless of everything else, is better than &lt;a href="https://wesbos.com/new-wesbos-website/#Pain-Points"&gt;the 25 minutes Wes Bos is experiencing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Hugo Pipeline Series – Intro</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Mon, 15 Jun 2020 07:01:26 +0000</pubDate>
      <link>https://dev.to/p4lm/hugo-pipeline-series-intro-5gf4</link>
      <guid>https://dev.to/p4lm/hugo-pipeline-series-intro-5gf4</guid>
      <description>&lt;h2&gt;
  
  
  Who is this for?
&lt;/h2&gt;

&lt;p&gt;This is the first part in a series of posts where I'll describe how I created my personal blog with a fair amount of work to achieve simplicity. The intended audience is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Myself (as documentation)&lt;/li&gt;
&lt;li&gt;Someone with their own &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt; blog, looking to improve automation&lt;/li&gt;
&lt;li&gt;Someone interesting in learning &lt;em&gt;one&lt;/em&gt; way of working with a Hugo blog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will be an overview of the different pieces I use, it won't be a detailed description or tutorial, but I'll try to link to libraries and concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/henriksommerfeld/blog-hugo"&gt;Repo is here&lt;/a&gt;. README shows how to run it locally&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://app.netlify.com/sites/henriksommerfeld/deploys/5edd46691261090008d5a8b5"&gt;build &amp;amp; deploy log is here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Site characteristics
&lt;/h2&gt;

&lt;p&gt;The site I'll be describing has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Annual running cost of &lt;strong&gt;nothing&lt;/strong&gt;, except for the domain name&lt;/li&gt;
&lt;li&gt;Static site (built with &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;400+ images&lt;/li&gt;
&lt;li&gt;&amp;lt; 2 minutes combined build &amp;amp; deploy time&lt;/li&gt;
&lt;li&gt;Custom domain with HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/p4lm/lazy-loading-images-in-hugo-45e0"&gt;Lazy loading of images&lt;/a&gt; (with blurry preview when JS is available)&lt;/li&gt;
&lt;li&gt;Search with &lt;a href="https://lunrjs.com/"&gt;Lunr&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Lightbox_(JavaScript)"&gt;Lightbox&lt;/a&gt; for images and code&lt;/li&gt;
&lt;li&gt;Dark/light mode&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/alpinejs/alpine"&gt;Alpine.js&lt;/a&gt; for custom JavaScript code&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://cypress.io/"&gt;Cypress&lt;/a&gt; tests that run automatically on &lt;a href="https://opensource.stackexchange.com/questions/352/what-exactly-is-a-pull-request#answer-380"&gt;pull requests&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/web/tools/lighthouse"&gt;Lighthouse&lt;/a&gt; audit results automatically run after deploy &lt;/li&gt;
&lt;li&gt;All code is in a &lt;a href="https://github.com/henriksommerfeld/blog-hugo"&gt;public repo on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Runs locally without Internet access&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What it doesn't have:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic data loading (except for comments and analytics)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Single-page_application"&gt;SPA&lt;/a&gt; characteristics (it has full page reloads)&lt;/li&gt;
&lt;li&gt;Offline availability (it's not a &lt;a href="https://web.dev/what-are-pwas/"&gt;PWA&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A proper &lt;a href="https://en.wikipedia.org/wiki/Content_management_system"&gt;CMS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Configurable UI, like themes intended for re-use tend to have&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fashion blogger (or other non-technical friend) might view this setup as horribly complicated, but that's because the complexity is hidden to the user in a typical blogging platform. &lt;/p&gt;

&lt;p&gt;Admittedly, I've spent some time on this over the years, with small adjustment every now and then, to achieve this level of sophistication and simplicity. It might sound arrogant with "sophistication" and "simplicity" but storing the content as text files instead of in a database and having a static site that can run from a &lt;a href="https://en.wikipedia.org/wiki/Content_delivery_network"&gt;CDN&lt;/a&gt; is &lt;em&gt;simple&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;It's &lt;em&gt;sophisticated&lt;/em&gt; in that the pipeline is automated and has tests in the same way as our professional applications we develop at work. I'll create a new branch, do code or design change and make a pull request (PR) to myself. If the tests that run automatically when a PR is created are green, I merge without hesitation. More details coming in future posts…&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>webdev</category>
      <category>blog</category>
    </item>
    <item>
      <title>8 JavaScript Recommendations to a Struggling Student</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Sun, 31 May 2020 09:48:16 +0000</pubDate>
      <link>https://dev.to/p4lm/8-javascript-recommendations-to-a-struggling-student-4l4d</link>
      <guid>https://dev.to/p4lm/8-javascript-recommendations-to-a-struggling-student-4l4d</guid>
      <description>&lt;p&gt;I'm sure there are thousands of posts on this topic already, but since this turned out to be mostly general advice, I might as well publish it to save a few keystrokes if I get a similar request in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;I was recently asked for advice by a student struggling with his web development assignment from school. This was an exercise from the level below university – &lt;em&gt;high school&lt;/em&gt;, &lt;em&gt;upper secondary school&lt;/em&gt;, or whatever it's called in your country. &lt;/p&gt;

&lt;p&gt;I got a zip file with the assignment as a PDF file and the code in its current state where the student was stuck. Unsurprisingly, it wasn't one specific thing that wasn't working with a clear question on how to solve that specific problem. There were errors in the web browser console and long functions with wrong indentation that made it all hard to understand.&lt;/p&gt;

&lt;p&gt;I've never been able to &lt;em&gt;"take a quick look"&lt;/em&gt; at a problem like this and give valuable feedback. To be able to give some helpful advice, I need to understand the assignment and see how far from a solution the current state is. In this case I ended up making my own implementation and then writing down my advice. We then had a screen sharing session walking through the problem step by step, until only some minor stuff remained to implement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fps7bldc8rlfozhwj4oy4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fps7bldc8rlfozhwj4oy4.jpg" alt="woman and man sitting in front of monitor"&gt;&lt;/a&gt;&lt;/p&gt;
Photo by &lt;a href="https://unsplash.com/@nesabymakers" rel="noopener noreferrer"&gt;
NESA by Makers&lt;/a&gt;



&lt;h2&gt;
  
  
  My general advice
&lt;/h2&gt;

&lt;p&gt;There was some specific advice I could give based on the assignment, but the general stuff was as follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Variable declarations
&lt;/h3&gt;

&lt;p&gt;A variable should be declared, either with &lt;code&gt;var&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt; or &lt;code&gt;const&lt;/code&gt;, but avoid&lt;br&gt;&lt;br&gt;
&lt;code&gt;var&lt;/code&gt; – see &lt;a href="https://hackernoon.com/why-you-shouldnt-use-var-anymore-f109a58b9b70" rel="noopener noreferrer"&gt;https://hackernoon.com/why-you-shouldnt-use-var-anymore-f109a58b9b70&lt;/a&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  2. Naming
&lt;/h3&gt;

&lt;p&gt;Giving &lt;em&gt;functions&lt;/em&gt; and &lt;em&gt;variables&lt;/em&gt; good names is one of the most difficult and most important things to get understandable code. Generally, I recommend using English names throughout. &lt;/p&gt;

&lt;p&gt;If it makes it easier for you to reason about the problem domain in your own language, then you might be better off using your native language, but if you do – use the whole alphabet (like umlauts) and not some crippled version of your language. Most importantly: be consistent. &lt;/p&gt;

&lt;p&gt;Here is a clear walkthrough of conventions you benefit from following: &lt;a href="https://www.robinwieruch.de/javascript-naming-conventions" rel="noopener noreferrer"&gt;https://www.robinwieruch.de/javascript-naming-conventions&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Indenting
&lt;/h3&gt;

&lt;p&gt;Indenting is, just like naming, something that helps or hinders the brain when reading code. It might feel trivial, but it does make a difference, maybe to your grade on this exercise as well.&lt;/p&gt;

&lt;p&gt;Code doesn't have to be pretty before it works, but wrong indentation can make you put something &lt;em&gt;inside&lt;/em&gt; instead of &lt;em&gt;outside&lt;/em&gt; a block (&lt;code&gt;{}&lt;/code&gt;) – and just like that, 10 minutes is wasted on troubleshooting.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Short functions
&lt;/h3&gt;

&lt;p&gt;Make sure your functions fit on the screen without scrolling. Especially if you have many levels of indentation, it's a sign you might be able to extract some of it into a new function, like the contents of an &lt;code&gt;if&lt;/code&gt; statement or a &lt;code&gt;for&lt;/code&gt; loop.&lt;/p&gt;

&lt;p&gt;What I'm reaching for here isn't code aesthetics that will give bonus points. I assume you're only interested in getting it to work at this point, and &lt;em&gt;that's&lt;/em&gt; when these general things help &lt;em&gt;me&lt;/em&gt; proceed in the right direction.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Limit state manipulation
&lt;/h3&gt;

&lt;p&gt;Use as few global variables (declared outside of any function) as possible and keep them in one place. Try to set them in as few places as possible. &lt;/p&gt;

&lt;h3&gt;
  
  
  6. Baby steps
&lt;/h3&gt;

&lt;p&gt;Keep the web browser developer tools (F12) open to spot errors in the console. If you have an error, stop what you're doing and fix the error. Save and test often.&lt;/p&gt;

&lt;p&gt;Make sure you address &lt;em&gt;one&lt;/em&gt; issue at a time. Even if you're not "done" with the feature you're working on, you can watch your progress by adding &lt;code&gt;console.log(variable)&lt;/code&gt; statements, inspecting the HTML in the browser or setting breakpoints and stepping through the code (debugging).&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Backup when something works
&lt;/h3&gt;

&lt;p&gt;Ideally you should use a version control system (like Git), but if you haven't been taught how to use that, you can always copy the code folder and give it a sensible name, whenever you've managed something to run like it should. It's just as easy to break something that worked before, as it's frustrating when it happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Google (verb)
&lt;/h3&gt;

&lt;p&gt;If you know what you want to achieve, but not how to type it out – google it, we all do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Having gone through the assignment, solving it together with the student in a two-hour screen sharing session, I conclude that methodical problem-solving skills is the most important. Of course you have to know the basics of the programming language you're using and have an understanding of the assignment to be solved, but there are no shortcuts. &lt;/p&gt;

&lt;p&gt;The other obvious insight is that apart from my first JavaScript specific advice on variables, this applies to most of coding – not only most programming languages, but most of programming you will ever do in your career.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Grass is Greener on the Other Side</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Mon, 04 May 2020 19:53:14 +0000</pubDate>
      <link>https://dev.to/p4lm/the-grass-is-greener-on-the-other-side-7g5</link>
      <guid>https://dev.to/p4lm/the-grass-is-greener-on-the-other-side-7g5</guid>
      <description>&lt;p&gt;Approaching 40 years old and two years since I changed direction as a software developer, I conclude that the grass really &lt;em&gt;is&lt;/em&gt; greener on the other side. Perhaps I should have jumped earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'm coming from
&lt;/h2&gt;

&lt;p&gt;Most of my career I've been a consultant working with &lt;a href="https://dot.net"&gt;.Net&lt;/a&gt; development and specialised in &lt;a href="https://www.microsoft.com/en-gb/microsoft-365/sharepoint/collaboration"&gt;SharePoint&lt;/a&gt; (described in &lt;a href="https://www.henriksommerfeld.se/getting-a-divorce-from-sharepoint/"&gt;&lt;em&gt;Getting a Divorce From SharePoint&lt;/em&gt;&lt;/a&gt;). When I read &lt;a href="https://itrevolution.com/book/the-phoenix-project/"&gt;The Phoenix Project&lt;/a&gt; I strongly identified with the pre-transformation description and the different characters described in that book. At one time, me and a colleague had a great laugh comparing our work situation to the film &lt;a href="https://www.imdb.com/title/tt3659388/"&gt;The Martian&lt;/a&gt; (where the main character is stuck alone on Mars, click the link and see the trailer to get a feeling for what I mean). Our job felt just as complex and challenging – and we were only maintaining a bloody website! To be fair, it was a website selling products for a couple of hundred million EUR annually, but still.&lt;/p&gt;

&lt;p&gt;Not all problems should be solved, some should be avoided. When things are complicated due to accidental technical debt rather than complex due to supporting a complex world, walk away. &lt;/p&gt;

&lt;p&gt;Another aspect of your career is how much you learn over time. Do I have x years of experience or 1 year of experience x times?&lt;/p&gt;

&lt;p&gt;Sample tech:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://visualstudio.microsoft.com/vs/"&gt;Visual Studio (Not responding)™&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;.Net Framework 4.x&lt;/li&gt;
&lt;li&gt;Team Foundation Server (using &lt;a href="https://medium.com/@thomaz.moura/tfs-version-control-is-dead-6d475e247389"&gt;TFVC&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;SharePoint Server&lt;/li&gt;
&lt;li&gt;MS SQL Server&lt;/li&gt;
&lt;li&gt;Windows Server&lt;/li&gt;
&lt;li&gt;Virtual machines weighing 100+ GB&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.lenovo.com/us/en/laptops/thinkpad/thinkpad-p/ThinkPad-P50/p/22TP2WPWP50"&gt;Lenovo P50&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;README for monthly "deploy weekend"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where I'm now
&lt;/h2&gt;

&lt;p&gt;Now I'm in &lt;em&gt;ops&lt;/em&gt;, part of a team named &lt;em&gt;Tooling&lt;/em&gt; that provides the infrastructure and tools for other development teams to run their applications on. I'm the least experienced in the team when it comes to infrastructure and the tools we use, so I learn a ton of new stuff all the time. Having taken a web interface for granted for almost all of my career, the primary user interface our team is building right now is a &lt;a href="https://www.w3schools.com/whatis/whatis_cli.asp"&gt;CLI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As a developer in our organisation, you create a new service (API, web app, etc.) by using our CLI. It will ask you a few questions: name and description for the service, what team owns it, select the upstream services it depends on etc. That takes care of all the boring stuff you &lt;em&gt;need&lt;/em&gt;, but don't want to deal with as an application developer, like logging, monitoring, TLS certificates etc.&lt;/p&gt;

&lt;p&gt;Naturally, the idea of &lt;em&gt;infrastructure as code&lt;/em&gt; isn't something you have to &lt;em&gt;sell&lt;/em&gt; to someone like me with a coding background, but seeing it in action is still fantastic. Even if this isn't feasible for a small company, we're certainly not as big as Google, Amazon, Microsoft or Facebook.&lt;/p&gt;

&lt;p&gt;I find it delightful to primarily work with open source products. We buy commercial software when we find that's the best option, but we don't buy a product that claims to do everything. There is a big difference in choosing the pieces that best fits the puzzle you're trying to assemble, vs buying a complete puzzle that you have to retrofit your existing pieces into.&lt;/p&gt;

&lt;p&gt;Sample tech:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://code.visualstudio.com/"&gt;Visual Studio Code&lt;/a&gt; (still avoid &lt;a href="https://www.vim.org/"&gt;Vim&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;JavaScript/TypeScript&lt;/li&gt;
&lt;li&gt;&lt;a href="https://golang.org/"&gt;Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/"&gt;Node JS&lt;/a&gt; with &lt;a href="https://koajs.com/"&gt;koa&lt;/a&gt; or &lt;a href="https://www.apollographql.com/"&gt;Apollo Server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt;, &lt;a href="https://www.terraform.io/"&gt;Terraform&lt;/a&gt;, &lt;a href="https://www.nomadproject.io"&gt;Nomad&lt;/a&gt;, &lt;a href="https://www.consul.io/"&gt;Consul&lt;/a&gt;, &lt;a href="https://www.jenkins.io/"&gt;Jenkins&lt;/a&gt;, &lt;a href="https://neo4j.com/"&gt;Neo4j&lt;/a&gt;, &lt;a href="https://grafana.com/"&gt;Grafana&lt;/a&gt;, &lt;a href="https://www.graylog.org/"&gt;Graylog&lt;/a&gt;, &lt;a href="https://prometheus.io/docs/introduction/overview/"&gt;Prometheus&lt;/a&gt;...&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://visualstudio.microsoft.com/vs/"&gt;Mob&lt;/a&gt; station with large TV and &lt;a href="https://ubuntu.com/"&gt;Ubuntu Linux&lt;/a&gt; &lt;a href="https://en.wikipedia.org/wiki/Next_Unit_of_Computing"&gt;NUC&lt;/a&gt; + personal &lt;a href="https://www.henriksommerfeld.se/settings-for-new-macbook-pro/"&gt;MacBook Pro&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next actions
&lt;/h2&gt;

&lt;p&gt;I will keep evaluating my learning and how fun I think my job is. In hindsight I should probably have made a change earlier, but I wasn't unhappy before either and I guess that's the tricky part when it isn't as clear as black and white. &lt;/p&gt;

&lt;p&gt;I hope I have contributed to better ways of working earlier in my career as well, but some things are hard to change within a specific role or organisation. Some people prefer stability and familiarity, but "git hooks" &lt;em&gt;are&lt;/em&gt; better than "deployment weekends"!&lt;/p&gt;

&lt;p&gt;Read (or listen to the audio books) &lt;a href="https://itrevolution.com/book/the-phoenix-project/"&gt;The Phoenix Project&lt;/a&gt; and &lt;a href="https://itrevolution.com/book/the-unicorn-project/"&gt;The Unicorn Project&lt;/a&gt; and listen to &lt;a href="https://www.infoq.com/presentations/Simple-Made-Easy/"&gt;Rich Hickey's talk &lt;em&gt;Simple Made Easy&lt;/em&gt;&lt;/a&gt; if you haven't, that's my next actions for you 😉&lt;/p&gt;

</description>
      <category>career</category>
      <category>opensource</category>
      <category>learning</category>
      <category>motivation</category>
    </item>
    <item>
      <title>Asuswrt-Merlin Firmware Update Checker</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Thu, 09 Apr 2020 13:31:26 +0000</pubDate>
      <link>https://dev.to/p4lm/asuswrt-merlin-firmware-update-checker-10hg</link>
      <guid>https://dev.to/p4lm/asuswrt-merlin-firmware-update-checker-10hg</guid>
      <description>&lt;p&gt;I have an Asus RT-AC68U router at home. &lt;a href="https://www.henriksommerfeld.se/firmware-update-notifications-for-my-asus-router"&gt;I've previously used the build-in update checker together with a notification script&lt;/a&gt; that ran on the router itself. Recently I noticed that I hadn't got any update notifications in a long time, a downside of silent failures. &lt;/p&gt;

&lt;p&gt;When I ran a manual check using the router's web interface, it just said: &lt;em&gt;"Temporarily unable to get the latest firmware information. Please try again later."&lt;/em&gt; It doesn't seem to be that temporary though.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;p&gt;The code is in &lt;a href="https://github.com/henriksommerfeld/asuswrt-merlin-update-check"&gt;this GitHub repo&lt;/a&gt; and the scheduling piece with cron is described a the end of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building my own update checker
&lt;/h2&gt;

&lt;p&gt;Since &lt;a href="https://www.asuswrt-merlin.net/"&gt;the project website of Asuswrt-Merlin&lt;/a&gt; presents the latest version in an easily parsable way, I decided to write my own checker using &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Web_scraping"&gt;screen scraping&lt;/a&gt;&lt;/em&gt; in NodeJS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version checker
&lt;/h3&gt;

&lt;p&gt;To find the latest version, I just looked at &lt;a href="https://www.asuswrt-merlin.net/"&gt;the website&lt;/a&gt;, inspected the HTML, installed the packages &lt;code&gt;request-promise&lt;/code&gt; and &lt;code&gt;cheerio&lt;/code&gt;, and finally extracted the version number of interest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request-promise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cheerio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getLatestStableVersion&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.asuswrt-merlin.net/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#block-currentrelease&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stableOthers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Others:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stableVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stableOthers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Others:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stableVersion&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Saving last checked version
&lt;/h3&gt;

&lt;p&gt;In order to know if there is a new version since my last check, I of course need to keep track of what the version was the last time I checked. I did this with a simple text file on disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedVersionFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./last-checked-version.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getLastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedVersionFilePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastCheckedVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedVersionFilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;saveLastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedVersionFilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Notifier
&lt;/h3&gt;

&lt;p&gt;I already had a working notification script using the service &lt;a href="https://pushover.net/"&gt;Pushover&lt;/a&gt; that I ported from Bash to NodeJS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request-promise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;sendPushoverNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.pushover.net/1/messages.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUSHOVER_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUSHOVER_USER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsedBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Pushover notification sent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Gluing it together
&lt;/h3&gt;

&lt;p&gt;By sending a notification both when there is no update and when an error occurs, I won't have any silent failures unless I made a mistake here somewhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getLatestStableVersion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./latest-version-checker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendPushoverNotification&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./notify.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getLastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;saveLastCheckedVersion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./localFile.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastCheckedVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getLastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main -&amp;gt; lastCheckedVersion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latestVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getLatestStableVersion&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latestVersion&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;lastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`🔔 New firmware version &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;latestVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is now available at 

      https://www.asuswrt-merlin.net/`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;sendPushoverNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;saveLastCheckedVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latestVersion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`🤷‍♂️ No firmware released. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;latestVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is the latest.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;sendPushoverNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main -&amp;gt; error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`⚠️ Router firmware update check failed`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;sendPushoverNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Scheduling the update checker
&lt;/h2&gt;

&lt;p&gt;I'm running this on a &lt;a href="https://www.raspberrypi.org/"&gt;RaspberryPi&lt;/a&gt; and it's scheduled to run once a week, 18:10 on Wednesdays. I found &lt;a href="https://crontab.guru"&gt;https://crontab.guru&lt;/a&gt; to be helpful for not mixing up the time settings.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;crontab -e&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 18 * * 3 /home/pi/router-update-check.sh &amp;gt;&amp;gt; /home/pi/router-update-check.log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The trickiest thing for me as a terrible Linux admin, was to get the cron scheduling working. Adding the output of &lt;code&gt;echo $PATH&lt;/code&gt; at the top of the script did the trick. Logging the output (to &lt;code&gt;router-update-check.log&lt;/code&gt; in this case), also helped.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;router-update-check.sh&lt;/code&gt; script contains the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/pi/.nvm/versions/node/v13.12.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/snap/bin

&lt;span class="nb"&gt;cd&lt;/span&gt; /home/pi/Code/asuswrt-merlin-update-check/
node ./main.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The result of running the script once every minute (while troubleshooting) showed up in my phone like this. I now have an update checker that I can only blame myself if it doesn't work. Great success!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--STq7eGRc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/jp3jkg4b9qe5olnvpi1j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--STq7eGRc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/jp3jkg4b9qe5olnvpi1j.jpg" alt="Pushover notifications on iOS"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>bash</category>
      <category>linux</category>
    </item>
    <item>
      <title>Code Quality and Automated Tests for Legacy Systems</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Thu, 12 Mar 2020 19:01:38 +0000</pubDate>
      <link>https://dev.to/p4lm/code-quality-and-automated-tests-for-legacy-systems-30mm</link>
      <guid>https://dev.to/p4lm/code-quality-and-automated-tests-for-legacy-systems-30mm</guid>
      <description>&lt;p&gt;My team recently had a discussion trying to agree on a few rules of thumb for our ambition level regarding code quality and automated tests for our legacy systems. These systems are business-critical, but planned to be replaced in a Big Bang fashion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z2_6xf30--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2auexutivj0rb9qr2tgq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z2_6xf30--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2auexutivj0rb9qr2tgq.jpg" alt="Mainframe"&gt;&lt;/a&gt;&lt;/p&gt;
Photo by &lt;a href="https://unsplash.com/@alexmotoc?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Alex Motoc&lt;/a&gt;



&lt;p&gt;One of my colleagues challenged our usual code of conduct of continuous improvement of code quality in regards to these systems. For a code base that has a set timeline for decommissioning, I agreed that we shouldn't strive to improve the code quality. This is especially true when we're talking about a code base that's fragile and where &lt;em&gt;any&lt;/em&gt; change is more or less risky.&lt;/p&gt;

&lt;p&gt;Our mission is first and foremost to keep these systems running, second to fix high priority bugs and where possible, make highly requested minor improvements. &lt;/p&gt;

&lt;h2&gt;
  
  
  Code quality
&lt;/h2&gt;

&lt;p&gt;The code of conduct we came up with for these systems, is something along these lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Strive for a code quality that won't bite us later&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Meaning, if we believe we will have to come back to a particular piece of code and make another change later – apply good practices so that we are nice to our future selves, but don't do any refactoring of surrounding code. If adding another &lt;code&gt;if&lt;/code&gt; statement solves the problem – do it and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated tests
&lt;/h2&gt;

&lt;p&gt;For automated tests, we said the following:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write a test when we're fixing a bug, if it's reasonable&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Creating an automated test or two as part of fixing a bug is great to avoid regressions later. But we also realise that for some systems it isn't worth the effort. This is when there are no existing tests, meaning no testing infrastructure in place. Some of our old systems have a lot of logic in &lt;em&gt;stored procedures&lt;/em&gt; in the database, and to add tests for that just doesn't make sense in our situation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write tests where it's making development easier&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The most obvious case for writing tests are when it helps development directly. This could be a validation function or some other non-trivial &lt;a href="https://en.wikipedia.org/wiki/Pure_function"&gt;pure function&lt;/a&gt; where unit tests make the function easier to write. So even if the tests are never run again, it would still be worth the effort.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write tests that have as good pay off as possible &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An example of this is UI tests that can catch many possible errors in relation to the amount of test code. This is of course given that we already have a stable set of tests, we don't want to spend a lot of time troubleshooting flaky UI tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion 🤷‍♂️
&lt;/h2&gt;

&lt;p&gt;Is this just common sense or too vague to be useful? Maybe it's vague, but given the diversity of the systems we're talking about here, we couldn't find a way to make it more concrete without making it system specific. Doesn't this apply to all systems? Well, sort of. I think the only relevant difference is the amount of time we're willing to spend on improving existing code – and that amount of time is more of a gut feeling than a discrete number. But in any case, I think it's good we had the discussion and hopefully we can waste less time trying to improve code that will die fairly soon anyway.&lt;/p&gt;

</description>
      <category>legacy</category>
      <category>testing</category>
      <category>codequality</category>
    </item>
    <item>
      <title>How I Tackled Parental Leave Boredom With Code</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Wed, 12 Feb 2020 10:07:32 +0000</pubDate>
      <link>https://dev.to/p4lm/how-i-tackled-parental-leave-boredom-with-code-42k7</link>
      <guid>https://dev.to/p4lm/how-i-tackled-parental-leave-boredom-with-code-42k7</guid>
      <description>&lt;p&gt;Approaching the end of my parental leave, I'll take a moment reflecting on how I tackled the occasional boredom of a longer absence from work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ronX7aSi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1lmvv5f2hudwewsiwned.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ronX7aSi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1lmvv5f2hudwewsiwned.jpg" alt="Stock image of dad reading book for child"&gt;&lt;/a&gt;&lt;/p&gt;
Photo by &lt;a href="https://unsplash.com/@picsea?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Picsea&lt;/a&gt;



&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I live in Sweden where we have good possibilities for taking long parental leaves. In my case it has been 7 months – 8 if I count the summer vacation. While I'm absolutely thankful for the possibility to stay home with my daughter and getting to know her, I'll never get a second chance etc etc 🙄, it's still boring as hell from time to time. Having an ocean of time, but all the time being consumed by baby care was a new and frustrating experience for me. &lt;/p&gt;

&lt;p&gt;What saved me from becoming &lt;em&gt;chairman of the bored&lt;/em&gt; was the small technology projects I could work on during the short periods of focus time (like when the child was asleep). Problem solving during stroller walks was also a great way to make progress even in pure AFK days (Away From Keyboard).&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked for me
&lt;/h2&gt;

&lt;p&gt;Looking back at these months, I'm quite satisfied with my accomplishments. In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/p4lm/netlify-cms-with-gatsby-best-option-with-some-quirks-3p5i"&gt;Rebuilt personal website for my wife&lt;/a&gt; from WordPress to Gatsby, saving us the annual fee for WordPress.com and ended up with a more personal site.&lt;/li&gt;
&lt;li&gt;Added &lt;a href="https://www.henriksommerfeld.se/dark-mode-learnings/"&gt;dark mode to my own blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Added &lt;a href="https://dev.to/p4lm/lazy-loading-images-in-hugo-45e0"&gt;lazy loading of images&lt;/a&gt; on my blog&lt;/li&gt;
&lt;li&gt;Built a &lt;a href="https://recept.netlify.com"&gt;family recipe collection&lt;/a&gt; site with a CMS&lt;/li&gt;
&lt;li&gt;Kept the daughter alive (happy and well) 👶🏻&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Small projects
&lt;/h3&gt;

&lt;p&gt;I think a key success factor was to start small, keep the scope smaller than what you think you'll manage to complete (because you'll be wrong, you'll always be able to do less than what you initially think). &lt;/p&gt;

&lt;h3&gt;
  
  
  MVP
&lt;/h3&gt;

&lt;p&gt;Define a &lt;em&gt;Minimal Viable Product&lt;/em&gt; (MVP) and keep track of &lt;em&gt;nice to have&lt;/em&gt; stuff separately. When you have other stakeholders, like with my wife's personal website, it can be a good idea to explain the concept of an MVP – what's simplest version I can build that you can use and we still can kill the old site?&lt;/p&gt;

&lt;p&gt;Once the new site was live, I could continue to do small improvements without time pressure (when the WordPress.com subscription was already cancelled). These were things that was easy to do without longer periods of focus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Have your laptop with you
&lt;/h3&gt;

&lt;p&gt;Since you'll need a bag with diapers, change of clothes, food, water and so on, you might as well throw in your laptop when leaving home. That way you can make use of the random chunks of time that appears when the kid falls asleep.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ensure you can take notes on the go
&lt;/h3&gt;

&lt;p&gt;When you get a great idea during a walk, it's nice to be able to easily catch that so that you can process it later. I carry the &lt;a href="https://www.google.com/search?q=david+allen+notetaker+wallet"&gt;David Allen Notetaker wallet&lt;/a&gt; (not sold any more) since many years, but I guess most people take notes on their phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;I'm not saying that everyone should do as much work related stuff as possible during their parental leave. For &lt;em&gt;you&lt;/em&gt; the best decision might be to disconnect from work as much as possible, but for &lt;em&gt;me&lt;/em&gt; this was a way of staying sane. Thanks for reading!&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>mentalhealth</category>
      <category>programming</category>
      <category>life</category>
    </item>
    <item>
      <title>Lazy Loading Images in Hugo</title>
      <dc:creator>Henrik Sommerfeld</dc:creator>
      <pubDate>Wed, 05 Feb 2020 11:47:01 +0000</pubDate>
      <link>https://dev.to/p4lm/lazy-loading-images-in-hugo-45e0</link>
      <guid>https://dev.to/p4lm/lazy-loading-images-in-hugo-45e0</guid>
      <description>&lt;p&gt;When writing another post, I realised that I hadn't documented/described my image &lt;a href="https://en.wikipedia.org/wiki/Lazy_loading"&gt;lazy loading&lt;/a&gt; implementation in &lt;a href="https://gohugo.io"&gt;Hugo&lt;/a&gt; anywhere, so here it comes. Full source code is in &lt;a href="https://github.com/henriksommerfeld/blog-hugo"&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first thing we need are &lt;a href="https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-srcset/"&gt;responsive images&lt;/a&gt;, not the thing you get when setting the width to 100% in CSS, but different versions of the same image in different resolutions so that the web browser can pick the best one (using &lt;code&gt;srcset&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing folder structure
&lt;/h2&gt;

&lt;p&gt;The structure I had before implementing this was all the Markdown files in the same folder with the images in a common &lt;code&gt;static/images&lt;/code&gt; folder. To use &lt;a href="https://gohugo.io/content-management/image-processing/"&gt;image processing&lt;/a&gt; in Hugo we need to use &lt;em&gt;&lt;a href="https://gohugo.io/content-management/page-resources/"&gt;Page Resources&lt;/a&gt;&lt;/em&gt;, meaning we have to create a folder for each post where we put both the Markdown file and the images related to that post.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content
 - articles
   - 2019-11-15-dark-mode.md
   - 2019-11-17-netlify-cms.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content
 - articles
   - 2019-11-15-dark-mode
     - index.md
     - jay-wennington-loAgTdeDcIU-unsplash.jpg
   - 2019-11-17-netlify-cms
     - hero-image.png
     - index.md
     - insert-image.png
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Writing a shortcode
&lt;/h2&gt;

&lt;p&gt;To be able to reference the images in the Markdown files, we need a &lt;em&gt;&lt;a href="https://gohugo.io/content-management/shortcodes/"&gt;shortcode&lt;/a&gt;&lt;/em&gt;. This is the trickiest part to explain since the code is a bit lengthy. This is because I have the possibility to pass parameters for whether or not to have a border, a custom width, a lightbox and caption. It might also be because my Go template skills are questionable.&lt;/p&gt;

&lt;p&gt;I use the shortcode in my &lt;code&gt;index.md&lt;/code&gt; file to reference the image like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hero-image.png"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;What happens in the shortcode implementation below is that I create a blurred version of the image resized to 48px wide (keeping the aspect ratio). This is the image you will see before a better version is fetched. This small image is embedded into the HTML as a Base64 encoded string, so that there is no extra request required to get the initial image.&lt;/p&gt;

&lt;p&gt;A few different versions are then generated, but only if the original image is larger than the version I'm trying to generate. I don't want to upscale an image (it will be blurry). The variable &lt;code&gt;$src_set&lt;/code&gt; is appended with each version and will contain all versions (sizes) when the HTML part begins.&lt;/p&gt;

&lt;p&gt;Shortcode &lt;code&gt;post-image.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;{{ $image := (.Page.Resources.GetMatch  (index .Params.image)) }}
{{ $alt := .Get "alt" }}
{{ $width := .Get "width" }}
{{ $borderless := .Get "borderless" }}
{{ $placeholder := ($image.Resize "48x q20") | images.Filter (images.GaussianBlur 6) }}
{{ $src := $image }}
{{ $src_set := ""}}

{{ $src_set = (print $image.RelPermalink " " $image.Width "w") }}
{{ $src := $image }}

{{ if ge $image.Width "500"}}
{{ $x_small := $image.Resize "500x" }}
{{ $src_set = (print $src_set ", "  $x_small.RelPermalink " 500w") }}
{{ end }}

{{ if ge $image.Width "800"}}
{{ $small := $image.Resize "800x" }}
{{ $src_set = (print $src_set ", " $small.RelPermalink " 800w") }}
{{ end }}

{{ if ge $image.Width "1200"}}
{{ $medium := $image.Resize "1200x" }}
{{ $src_set = (print $src_set ", " $medium.RelPermalink " 1200w") }}
{{ end }}

{{ if gt $image.Width "1500"}}
{{ $large := $image.Resize "1500x" }}
{{ $src_set = (print $src_set ", " $large.RelPermalink " 1500w") }}
{{ end }}

{{ $border_class := "image-border" }}
{{ if $borderless}}
{{ $border_class = "" }}
{{ end }}


&lt;span class="nt"&gt;&amp;lt;noscript&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;figure&lt;/span&gt;&lt;span class="nc"&gt;.lazy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;figure&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"{{ $border_class }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    {{ if .Get "lightbox" }}
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;'{{ $image.RelPermalink }}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      {{ end }}
      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ $src.RelPermalink }}"&lt;/span&gt; &lt;span class="err"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt; &lt;span class="err"&gt;}}&lt;/span&gt;&lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"{{$width}}"&lt;/span&gt;&lt;span class="err"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;}}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      {{ if .Get "lightbox" }}
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    {{ end }}
    &lt;span class="nt"&gt;&amp;lt;figcaption&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;{{ .Inner }}&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/figcaption&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/noscript&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;figure&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"{{ $border_class }} lazy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {{ if .Get "lightbox" }}
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;'{{ $image.RelPermalink }}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    {{ end }}
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lazyload"&lt;/span&gt; &lt;span class="na"&gt;data-sizes=&lt;/span&gt;&lt;span class="s"&gt;"auto"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ $src.RelPermalink }}"&lt;/span&gt; &lt;span class="err"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt; &lt;span class="err"&gt;}}&lt;/span&gt;&lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"{{$width}}"&lt;/span&gt;&lt;span class="err"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"data:image/jpeg;base64,{{ $placeholder.Content | base64Encode }}"&lt;/span&gt; &lt;span class="na"&gt;data-src=&lt;/span&gt;&lt;span class="s"&gt;"{{ $src.RelPermalink }}"&lt;/span&gt;
      &lt;span class="na"&gt;data-srcset=&lt;/span&gt;&lt;span class="s"&gt;"{{ $src_set }}"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"{{ $image.Width }}"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"{{ $image.Height }}"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"{{ $alt }}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    {{ if .Get "lightbox" }}
  &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  {{ end }}
  {{ if .Inner }}
  &lt;span class="nt"&gt;&amp;lt;figcaption&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;{{ .Inner }}&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/figcaption&amp;gt;&lt;/span&gt;
  {{ end }}
&lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding Javascript
&lt;/h2&gt;

&lt;p&gt;There are a few CSS classes set here that acts as a signal to different Javascript features. &lt;code&gt;lightbox&lt;/code&gt; is such a thing, but the interesting one here is &lt;code&gt;lazyload&lt;/code&gt;. I use a Javascript library called &lt;a href="https://github.com/aFarkas/lazysizes#readme"&gt;lazysizes&lt;/a&gt; that is included in my Javascript bundle and I have created the HTML to work with that library.&lt;/p&gt;

&lt;p&gt;You might have noticed that the code above looks somewhat duplicated, that's because I have a &lt;code&gt;noscript&lt;/code&gt; tag for those with Javascript disabled. In that case, a srcset tag is still used to provide the best image, but without the lazy loading.&lt;/p&gt;

&lt;p&gt;There is no "blur up effect" here. That's a conscious decision, since this is a bit simpler to implement and feels faster, in my opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance consideration
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://gohugo.io/content-management/image-processing/"&gt;Hugo's documentation for image processing&lt;/a&gt; it's clearly stated that it's recommended to include the generated images in source control. This is to avoid generating the same images over and over again. On a large site with lots of images, this can make a big difference.&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
