<?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: Luke Harris</title>
    <description>The latest articles on DEV Community by Luke Harris (@lkhrs).</description>
    <link>https://dev.to/lkhrs</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%2F126271%2F36d68468-a5af-43fc-8ddf-eb790b297af0.jpeg</url>
      <title>DEV Community: Luke Harris</title>
      <link>https://dev.to/lkhrs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lkhrs"/>
    <language>en</language>
    <item>
      <title>PurgeCSS with Hugo</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Sat, 29 Oct 2022 17:59:15 +0000</pubDate>
      <link>https://dev.to/lkhrs/purgecss-with-hugo-3fha</link>
      <guid>https://dev.to/lkhrs/purgecss-with-hugo-3fha</guid>
      <description>&lt;p&gt;Over the past several months I slowly rebuilt the theme for my site with &lt;a href="https://picocss.com"&gt;Pico.css&lt;/a&gt;, with the goal of eliminating the &lt;code&gt;div&lt;/code&gt; soup I had. This past week, my site stopped building altogether because of a dependency issue&lt;sup id="fnref:1"&gt;1&lt;/sup&gt;, and I polished up the theme last night for deployment.&lt;/p&gt;

&lt;p&gt;One win from this rebuild is that I finally got &lt;a href="https://purgecss.com"&gt;PurgeCSS&lt;/a&gt; and &lt;a href="https://cssnano.co"&gt;cssnano&lt;/a&gt; working with Hugo as part of my build pipeline. Although the savings were small, in the range of 1-2kb after Brotli compression, it was bugging me that it wasn’t working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first insight&lt;/strong&gt; was finding out I had Hugo’s &lt;code&gt;.Resources.PostProcess&lt;/code&gt; and &lt;code&gt;.Resources.PostCSS&lt;/code&gt; functions flipped in my head — the correct usage is to run &lt;code&gt;.Resources.PostCSS&lt;/code&gt; on your input from Hugo Pipes, and then tag the output with &lt;code&gt;.Resources.PostProcess&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tagging the output with PostProcess tells Hugo not to run that file through PostCSS until after it’s built the pages, which is important because PurgeCSS runs through the HTML (or &lt;code&gt;hugo_stats.json&lt;/code&gt;) to figure out which CSS selectors are used on the pages, and which ones it can get rid of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second insight&lt;/strong&gt; was that Hugo’s built-in selector detector (new band name) wasn’t catching the conditional selectors used in Pico, like &lt;code&gt;:where&lt;/code&gt; and &lt;code&gt;:is&lt;/code&gt;, resulting in significant savings in file size while leaving my site looking like it drove the wrong lane down a dirt road.&lt;/p&gt;

&lt;p&gt;To fix that, I stopped using &lt;code&gt;hugo_stats.json&lt;/code&gt; for PurgeCSS and used the output folder of HTML files instead. Then I added a whitelist to my &lt;code&gt;postcss.config.js&lt;/code&gt; based on &lt;a href="https://ttntm.me/blog/hugo-tailwind-boilerplate/"&gt;Tom’s post&lt;/a&gt;. Here’s the file in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;purgecss&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;@fullhuman/postcss-purgecss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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="s2"&gt;cssnano&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;purgecss&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&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;./public/**/*.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;defaultExtractor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;/&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;!:&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&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="na"&gt;safelist&lt;/span&gt;&lt;span class="p"&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;:hover&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;:focus&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;button&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;button:focus&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;:where&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;:is&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="p"&gt;};&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And the Hugo partial for processing the SCSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;tocss&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt; &lt;span class="s"&gt;"enableSourceMap"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hugo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsProduction&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;tocss&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt; &lt;span class="s"&gt;"enableSourceMap"&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="s"&gt;"output"&lt;/span&gt; &lt;span class="s"&gt;"compressed"&lt;/span&gt; &lt;span class="p"&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;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;styles&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;"scss/styles.scss"&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;ToCSS&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;tocss&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="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hugo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsProduction&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;link&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $styles.RelPermalink }}"&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&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;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hugo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsProduction&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;css&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;minify&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;postCSS&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;PostProcess&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;link&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $css.RelPermalink }}"&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&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;Now the only issue left to solve is figuring out why PostCSS doesn’t work in GitHub Actions.&lt;/p&gt;

&lt;p&gt;There’s a few nitpicks I have with Pico.css, I might swap it out for &lt;a href="https://simplecss.org"&gt;Simple.css&lt;/a&gt; now that my site uses semantic HTML. And I’m planning yet another redesign with vanilla CSS — I’ve built two sites that way recently and it’s been wonderful writing plain CSS again, with new features like &lt;code&gt;calc()&lt;/code&gt; and variables.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;Bootstrap 5 now uses SASS features that are incompatible with Hugo’s SASS processor, and I had no luck pinning the working version. ↩︎
&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Using variables in Jinja include statements</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Sat, 22 Oct 2022 21:47:26 +0000</pubDate>
      <link>https://dev.to/lkhrs/using-variables-in-jinja-include-statements-27k1</link>
      <guid>https://dev.to/lkhrs/using-variables-in-jinja-include-statements-27k1</guid>
      <description>&lt;p&gt;I’m working on a project that uses &lt;a href="https://getpelican.com"&gt;Pelican&lt;/a&gt; as the static site generator. Pelican uses the &lt;a href="https://jinja.palletsprojects.com"&gt;Jinja&lt;/a&gt; templating language, which looks similar to Liquid and Nunjucks.&lt;/p&gt;

&lt;p&gt;Inside a &lt;code&gt;for&lt;/code&gt; loop, I have a set of SVGs that I need to match to a variable in the frontmatter for each page, with the SVG code itself in the HTML output. I can’t use &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; here, because I need to set the fill color for the SVGs with CSS.&lt;/p&gt;

&lt;p&gt;I tried to use the &lt;code&gt;include&lt;/code&gt; tag like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;include&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nv"&gt;icons&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="err"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;variable&lt;/span&gt; &lt;span class="err"&gt;}}.&lt;/span&gt;&lt;span class="nv"&gt;svg&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;…which didn’t work, and led to a lot of time spent trying other methods, like writing a macro for each icon with an &lt;code&gt;if&lt;/code&gt; statement chain worthy of &lt;a href="https://thedailywtf.com/"&gt;TheDailyWTF&lt;/a&gt;, dictionary comparison, and other complicated nonsense.&lt;/p&gt;

&lt;p&gt;After reading the docs for the 10th time, I happened to scroll down to the section on &lt;a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#other-operators"&gt;Other Operators&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt; (tilde) Converts all operands into strings and concatenates them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{{ "Hello " ~ name ~ "!" }}&lt;/code&gt; would return (assuming name is set to ‘John’) Hello John!.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And came up with this elegant solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"icons/"&lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt; &lt;span class="nv"&gt;variable&lt;/span&gt; &lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="s2"&gt;".svg"&lt;/span&gt; &lt;span class="k"&gt;ignore&lt;/span&gt; &lt;span class="k"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Boom!&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ignore missing&lt;/code&gt; tells the templating engine to continue when &lt;code&gt;variable&lt;/code&gt; isn’t set.&lt;/p&gt;

&lt;p&gt;Here’s the full loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;pages&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
    &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;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;SITEURL&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;p.url&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"icons/"&lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt; &lt;span class="nv"&gt;p.icon&lt;/span&gt; &lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="s2"&gt;".svg"&lt;/span&gt; &lt;span class="k"&gt;ignore&lt;/span&gt; &lt;span class="k"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
      &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;p.title&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endfor&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;

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

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Setting Up Dynatrace</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Fri, 30 Sep 2022 22:27:04 +0000</pubDate>
      <link>https://dev.to/lkhrs/setting-up-dynatrace-fgh</link>
      <guid>https://dev.to/lkhrs/setting-up-dynatrace-fgh</guid>
      <description>&lt;p&gt;I’m learning more about DevOps stuff, so I’ve been looking into monitoring solutions used in large enterprise environments.&lt;/p&gt;

&lt;p&gt;Dynatrace is an “all-in-one intelligence platform”, which is marketing-speak for monitoring, analytics, and reporting, though it does a bit more than that. Enterprises use it to monitor system load statistics from servers, containers, and cloud platforms, as well as individual application monitoring, tracing, network performance, and continuous delivery testing. It supports piping in metrics from other providers, too.&lt;/p&gt;

&lt;p&gt;Once set up, metrics are available in one place to build dashboards, analyze with queries, generate reports, and other businessy things.&lt;/p&gt;

&lt;p&gt;The documentation for &lt;a href="https://www.dynatrace.com/support/help/get-started"&gt;getting started&lt;/a&gt; could be better, so I’m writing down my setup process for testing and some cool things you can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;The first thing you’ll need to do is sign up for the free trial. It’s 14-day, no card required, so you won’t get stuck with a surprise enterprise-level bill for monitoring your 2-player Factorio server.&lt;/p&gt;

&lt;p&gt;For testing, I recommend the virtual machine route, or &lt;a href="https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/docker"&gt;Docker container&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install in a VM
&lt;/h3&gt;

&lt;p&gt;I installed the Dynatrace OneAgent on an Ubuntu VM that I spun up using &lt;a href="https://multipass.run"&gt;Multipass&lt;/a&gt;. If you follow the same route I did and you have an M1 Mac, make sure you use the ARM (AARCH64) version of OneAgent, otherwise it won’t install.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Once you’ve got your VM or dumpster-sourced Linux box running, log in to your Dynatrace account. If the panel doesn’t automatically load the “Deploy Dynatrace” page, you’re about to discover the UX wonder called “The Dynatrace Menu”. Click the hamburger icon at the top left of the page, and then head to &lt;strong&gt;Favorites&lt;/strong&gt; → &lt;strong&gt;Deploy Dynatrace&lt;/strong&gt; :&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930183602257.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8r2CDT7I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930183602257_hu8ae27336eefac1dd2d12f33a8fbae282_89650_1978x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Once you’re there, click the &lt;strong&gt;Start installation&lt;/strong&gt; button in the bottom right corner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the &lt;strong&gt;Install OneAgent&lt;/strong&gt; page, select the &lt;strong&gt;Linux&lt;/strong&gt; platform, and you’ll be presented with this page:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930184529122.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oj9KkvGe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930184529122_hu28382e9c4d7c2661e1f6ffc8cc3bbeda_103520_1844x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;You’ll need to generate a PaaS token, which you can do by clicking the &lt;strong&gt;Create token&lt;/strong&gt; button on the right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once you have a PasS token, the installer page will give you three commands to copy and paste into your VM’s shell, in order. The first one downloads the installer, the second one verifies the signature, and the last one runs the installer. You’ll need to run the last one as root.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When it’s done installing, your shell should look similar to mine:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930185415805.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zQzjKKjQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930185415805_hu2662f6adce09c25e0919ea3645230aa1_276385_1524x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can verify OneAgent is running with &lt;code&gt;systemctl status oneagent.service&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930185752544.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tekg0ZZ5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930185752544_hu69d443d00e67d54beff101dcdaaf2bb5_256472_1524x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Viewing host performance
&lt;/h2&gt;

&lt;p&gt;Back to your browser, click the &lt;strong&gt;Show deployment status&lt;/strong&gt; button on the lower right of the page, and after a small delay your VM will show up on the page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930190051135.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FvrgRq83--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930190051135_hu7d094f1234660ff86bbe254ad8917f7b_66064_1458x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click your host to head over to the details page, where you’ll see some quick stats like CPU, memory usage, and availability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930190709856.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yEmsBu6w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930190709856_hue0e9bf69d2a75dd46222d3a89ccefc69_222724_2422x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="687"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you see the “improved version” banner at the top of the page, enable it for the new panel design. Now the page should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930191253705.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iwKNDO7D--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930191253705_hua9a560e9015f2c1959add02f75910958_185170_2108x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="752"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you’ve got some pretty graphs! My favorite part though is the process analysis area, which saves me a trip to &lt;code&gt;htop&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930191905104.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--knwiT56P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930191905104_hu287d02d4e5f29332c695a8889cdec1bd_103128_1782x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also dive into a single process and see its usage history:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930192009335.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oACtRSF8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930192009335_hu2bf47b3a080bf97d64746a85e5250879_90589_1770x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with dashboards
&lt;/h2&gt;

&lt;p&gt;Now imagine you’ve got several hundred of these VMs running (I recommend not imagining the bill). Going to each page is inefficient at best, so let’s make a dashboard to view infrastructure status at a glance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a new dashboard
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the Dynatrace Menu (sidebar on the left)&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Favorites&lt;/strong&gt; → &lt;strong&gt;Dashboards&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930193536184.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ke0wGg1c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930193536184_hu33fe81d46836c29f221a2ac963e00b9b_268084_2300x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Above the dashboards table, click the &lt;strong&gt;Create dashboard&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Enter a dashboard name&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now you should have a page like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930202645029.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xfZv_VcS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930202645029_hu8c77317d5ae6bb778e51be22398c5508_50670_2304x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="717"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s add some widgets! The official tutorial recommends host health and CPU usage, so let’s start with those.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the sidebar on the right, find the &lt;strong&gt;Host health&lt;/strong&gt; widget&lt;/li&gt;
&lt;li&gt;Drag and drop anywhere on the dot matrix&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By default, this shows a hexagon per host. You can get more advanced with it and have multiple widgets for different groups, but I won’t get into that right now.&lt;/p&gt;

&lt;p&gt;I added some network widgets, so now my dashboard looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930203506110.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GzP8_qSz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930203506110_hu2bba36abccd59dac4639b6c0ce8f8549_32510_1724x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can add CPU usage dashboard widgets for individual hosts, so let’s add one now.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Infrastructure&lt;/strong&gt; → &lt;strong&gt;Hosts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Open the host we set up earlier&lt;/li&gt;
&lt;li&gt;Make sure you’re using the new dashboard, as recommended earlier&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Host performance&lt;/strong&gt; group, find &lt;strong&gt;CPU usage&lt;/strong&gt; , and click the three dot (•••) button to open the menu&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Pin to dashboard&lt;/strong&gt; , and choose the dashboard we created earlier&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930203928362.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EIjMOt4Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930203928362_hub463db9646a7fcd104c912b4ca17dfa7_25142_988x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now click the &lt;strong&gt;Open dashboard&lt;/strong&gt; button to view the new CPU widget:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930204104587.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YfMD8Y3R--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930204104587_hue6e3bf17f4459178cc0c3ebda0f80c23_75832_2310x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced widgets
&lt;/h3&gt;

&lt;p&gt;Let’s add some widgets for service-level objective (SLO) monitoring and server response time.&lt;/p&gt;

&lt;p&gt;For the SLO widget:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find and add the &lt;strong&gt;Service-level objective&lt;/strong&gt; widget to the dashboard&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By default there are no SLOs created yet, so let’s go define one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using the Dynatrace Menu, navigate to &lt;strong&gt;Cloud Automation&lt;/strong&gt; → &lt;strong&gt;Service-level objectives&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930205221748.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vtwKhWKv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930205221748_huce85373d7b2ff75e5ee08e54b47b108b_53492_2318x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since our test VM has no services running yet, let’s manually configure an SLO.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Configure SLOs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add new SLO&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give the SLO a name&lt;/li&gt;
&lt;li&gt;For the metric expression field, use &lt;code&gt;builtin:host.availability&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save changes&lt;/strong&gt; in the floating modal on the bottom of the screen&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we need to hook up our SLO widget on the dashboard to the SLO we just created.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the dashboard, click the dropdown chevron on the SLO widget and click &lt;strong&gt;Edit tile&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;In the sidebar on the right, select the SLO we just created:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930205719351.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tS-ALvgn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930205719351_hu9ac655573ac3648ff43f24ccddbb553a_12372_632x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="632" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Turn off &lt;strong&gt;Custom timeframe&lt;/strong&gt; , otherwise the default of one week will reflect poorly on our young VM&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Done&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we can keep an eye on how well we’re hitting our SLO:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930210124885.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--D62k9HFt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930210124885_huc50654711c8f2e6c817f05a6c94f1259_10959_624x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="624" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wow, we’re failing to hit our availability metrics. The default timeframe is 7 days though, and our VM is less than an hour old at this point, so let’s adjust the timeframe: At the top right of the page, after the green &lt;strong&gt;Deploy Dynatrace&lt;/strong&gt; button, adjust the timeframe to &lt;strong&gt;Last 30 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930210324947.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T6zPMN5a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930210324947_hub13bf85c8dbc53e10f0d8b4c280d6a66_10050_616x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="616" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There we go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring a container
&lt;/h2&gt;

&lt;p&gt;Dynatrace automatically detects new Docker containers and makes them available for monitoring. Let’s spin up a WordPress container.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Docker in your VM with &lt;code&gt;sudo apt install docker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add your user to the &lt;code&gt;docker&lt;/code&gt; group:

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sudo usermod -a -G docker luke&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;newgrp docker&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;Pull the WordPress image: &lt;code&gt;docker pull wordpress&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run the container: &lt;code&gt;docker run --name super-hexablog -d wordpress&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To view stats about the container in Dynatrace, wait about 2 minutes, and then head over to &lt;strong&gt;Infrastructure&lt;/strong&gt; → &lt;strong&gt;Containers&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930214812401.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TxFhmQBo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930214812401_hu4c48c49a60d25af244c3335561b57872_55481_2370x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking our &lt;code&gt;wordpress&lt;/code&gt; container takes us to a performance metrics page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930215055489.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L37SaA8N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930215055489_hu90107f2940eef89aa3ba9bdcf1c5a7bc_71723_2278x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="710"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up problem alerting
&lt;/h2&gt;

&lt;p&gt;I want to get notified when my web server crashes, so let’s set up some basic problem alerting.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Head to &lt;strong&gt;Infrastructure&lt;/strong&gt; → &lt;strong&gt;Technologies&lt;/strong&gt; → &lt;strong&gt;Apache HTTP Server&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Settings&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Availability monitoring&lt;/strong&gt; in the sidebar&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Process group availability monitoring&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Open a new problem&lt;/strong&gt; , select &lt;strong&gt;If any process becomes unavailable&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And I’d like to get emailed when things catch on fire:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Manage&lt;/strong&gt; → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Integration&lt;/strong&gt; → &lt;strong&gt;Problem notifications&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add notification&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Notification type&lt;/strong&gt; , select &lt;strong&gt;Email&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save changes&lt;/strong&gt; in the floating modal at the bottom of the page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930224131159.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PVg2o9Iv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930224131159_huee2e7dfdf5ac4e2382a7083b3435afdc_39685_1732x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="906"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s test it out! In our VM, run &lt;code&gt;docker stop super-hexablog&lt;/code&gt;. Within about a minute, you should see a red notification at the top of the page. Clicking it takes you to the &lt;strong&gt;Problems&lt;/strong&gt; view:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930224906642.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Lfu0-mzB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930224906642_hud95ce060f0650bf9d7718e748956cfee_73417_2290x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also drill down into problems and see what Dynatrace thinks is the root cause:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930225145868.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B3smzP24--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/09/setting-up-dynatrace/image-20220930225145868_hu4d7ad124bb208619f10e8cbcbd22bab4_103407_2280x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of Dynatrace" width="880" height="668"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I really like how the Dynatrace agent can detect new things on a host with no configuration needed. And it’s easy to go from a bird’s-eye view of your entire fleet to analyzing the details of an individual process.&lt;/p&gt;

&lt;p&gt;I plan to look at Datadog next. Prometheus + Grafana looks like a great open source option, I’m interested in seeing how they all compare. I’ve seen PromGraf used a lot for homelab setups.&lt;/p&gt;

&lt;p&gt;&lt;a href="//mailto:hi@lkhrs.com?subject=Re:%20Setting%20Up%20Dynatrace"&gt;Reply via email&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Interstitial Journaling</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Fri, 19 Aug 2022 22:58:34 +0000</pubDate>
      <link>https://dev.to/lkhrs/interstitial-journaling-2g94</link>
      <guid>https://dev.to/lkhrs/interstitial-journaling-2g94</guid>
      <description>&lt;p&gt;For the last 30 days, I have been writing time-stamped lists of thoughts and ideas into a note for the day. I’ve noticed a big improvement in my mood and focus, and I spend less time trying to pick up where I left off.&lt;/p&gt;

&lt;p&gt;Productivity enthusiasts recommend writing a journal entry, or daily note, every day. Some apps, like Logseq, are built around this idea, and Obsidian (the one I use) has a core plugin for this.&lt;/p&gt;

&lt;p&gt;However, a daily note in the style of a journal entry at the end of the day didn’t work for me. I often missed days, or struggled to recall the events of the day. I switched to a method called &lt;a href="https://nesslabs.com/interstitial-journaling"&gt;interstitial journaling&lt;/a&gt;, and I’ve been able to stick with it for a month so far, which is huge!&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;My daily note starts out with a list of goals I want to get done today, not necessarily everything on my todo list for the day. Just big ticket items or things that would make a huge difference in my mood if I got them knocked out today.&lt;/p&gt;

&lt;p&gt;Then the rest of the note is a bullet-point list, with each list item starting with the current time.&lt;sup id="fnref:1"&gt;1&lt;/sup&gt; I write about whatever I’m feeling, what I’m working on right now or think I should be working on, random thoughts, references to other notes, etc. I don’t worry about wording or spelling; it’s just a place to offload.&lt;/p&gt;

&lt;p&gt;These notes get rather long, so to help keep me on task, I put the daily goals underneath the notes. That way they’re always visible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Notes&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;03:09 writing a blog post on interstitial journaling&lt;/li&gt;
&lt;li&gt;03:15 wow it’s after 0300, why am I still awake&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Goals&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Finish up post on interstitial journaling&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s important to note, is a phrase with four words. You don’t &lt;em&gt;have&lt;/em&gt; to use a PKM app like Obsidian to take these notes, you can use anything at your disposal. Text file, a crumpled back-pocket notepad, napkin, body tattoo—these all work.&lt;/p&gt;

&lt;p&gt;Where apps like Obsidian come in handy is the ability to link to other notes. So when I’m writing down thoughts about a project I’m working on, I add a reference to a note for that project. Same for software I’m thinking about, blog posts I’m writing, and people.&lt;/p&gt;

&lt;p&gt;These daily notes then show up as backlinks in Obsidian, and part of the local graph, so you can go back and look at your thoughts or events about a specific subject. I keep the backlinks and local graph as a sidebar in Obsidian, here’s what this blog post looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/08/interstitial-journaling/obsidian.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LGRVmvt2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/08/interstitial-journaling/obsidian_hue0887cd82211505f0f97fb0a6779a5b0_146656_2206x0_resize_q85_bgffffff_box_3.jpg" alt="Screenshot of the Obsidian app showing this blog post. On the right, there is a sidebar showing a list of notes and quotes that reference this post." width="880" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;It’s helped me stay on task and resume where I left off when I return to my desk. I found myself exploring fewer rabbit holes, because writing down a brief thought about it is enough most of the time. And at the end of the day when I feel like I haven’t gotten much done, I can squash that feeling by reading through the day’s events and helping me realize, actually, I did a lot.&lt;/p&gt;

&lt;p&gt;Another way to think about it: it’s like live-tweeting into a note. Like when I used to tweet 100+ times a day in 2012.&lt;/p&gt;

&lt;p&gt;A few of these daily notes are huge—the first day’s note had roughly 1,500 words and 8,000 characters. There’s been several more that have reached 1,200+ words, but my average daily note is around 400-600 words. Similar to my blog’s &lt;a href="https://www.lkhrs.com/stats/"&gt;average word count&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Anyway, I recommend giving interstitial journaling a shot! And I would love to hear how it went for you.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;Obsidian makes this easy with a keyboard shortcut to insert the current time ↩︎
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="//mailto:%7B%7B%20.Site.Author.email%20%7D%7D?subject=Re:%20%E2%80%9CInterstitial%20Journaling%E2%80%9D"&gt;Reply via email&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Measuring terminal latency</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Wed, 06 Jul 2022 18:01:37 +0000</pubDate>
      <link>https://dev.to/lkhrs/measuring-terminal-latency-26m7</link>
      <guid>https://dev.to/lkhrs/measuring-terminal-latency-26m7</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Out of the six terminal apps I tested on macOS, kitty has the lowest input latency at 29.2ms. Terminal.app and WezTerm follow closely behind, and kitty has a small lead over Alacritty and iTerm2. When tuned, kitty is twice as fast as the latter two, and tied with Sublime Text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason
&lt;/h2&gt;

&lt;p&gt;I am using the &lt;a href="https://github.com/ayu-theme"&gt;Ayu&lt;/a&gt; theme for &lt;a href="https://fishshell.com"&gt;fish&lt;/a&gt; and Vim. When installing it, I found out that Terminal.app doesn’t support 24-bit color, which led me on a wild rabbit hole of discovery for a new terminal emulator that supported 24-bit color and didn’t suck.&lt;/p&gt;

&lt;p&gt;I ended up with 3 emulators that I liked: WezTerm, iTerm2, and kitty. Unable to pick between them, I decided to benchmark their input latency to aid my decision-making, though I’m still not sure what to use. I’ll come back to that later in the post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://isitsnappy.com"&gt;Is It Snappy?&lt;/a&gt; (IIS) for iOS to take slow-motion videos of the screen while pressing keys on my keyboard. I took 2-3 videos of each app and selected input/output markers in IIS. Some of the videos had some impossible delays, so I had filter out the bad ones and use the data that was closer to what I was expecting.&lt;/p&gt;

&lt;p&gt;For example, Terminal.app showed a whopping 800ms delay during my first measurement, but later measurements were within a 10ms deviation of the result portrayed here. I think it’s because it was hard to see when the key was fully pressed.&lt;/p&gt;

&lt;p&gt;When setting the output marker in IIS, I waited for the screen to completely finish drawing the typed character &lt;em&gt;and&lt;/em&gt; the new position of the cursor, to account for input latency and pixel response time.&lt;/p&gt;

&lt;p&gt;Because of these variables, and other things unique to my setup, &lt;strong&gt;this data is not accurate&lt;/strong&gt;. It’s close enough for my use, but I encourage you to take your own measurements.&lt;/p&gt;

&lt;p&gt;When setting things up for testing, I used the same setup I would use every day, which means custom fonts, themes, and editor plugins. I’m not trying to find the fastest vanilla terminal, I’m trying to find the best balance of performance and features.&lt;/p&gt;

&lt;p&gt;I set the size of the terminals at 90x30 columns/rows, and I placed them as close to the vertical middle of the display as possible. Some of the terminals had custom fonts and icons loaded, such as Meslo in Terminal.app, and JetBrains Mono in the others. WezTerm has its own icon glyph set. I added Meslo as a backup font to iTerm2 to support all the icons my CLI apps use. Font ligatures are on by default in WezTerm, but disabled in iTerm2.&lt;/p&gt;

&lt;p&gt;I tuned kitty’s performance and ran a separate test, labeled “ &lt;strong&gt;kitty - unlimited&lt;/strong&gt; ”. These are the settings I used, from the &lt;a href="https://sw.kovidgoyal.net/kitty/performance/"&gt;docs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1# 150~ FPS for MBP display (untested)
2repaint_delay 8
3
4# Remove artificial input delay
5input_delay 0
6
7# turn off vsync
8sync_to_monitor no

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

&lt;/div&gt;



&lt;p&gt;The bulk of my time spent in the terminal is text editing, so each terminal in the test ran Vim. I also tested the graphical text editors I have installed, as I was curious to see if Vim could beat Sublime Text in input latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specs
&lt;/h2&gt;

&lt;p&gt;I’m running macOS 12.4 Monterey on a 2021 MacBook Pro with an M1 Pro CPU and 16GB of RAM. It sits in clamshell mode while hooked up to two 4k 60Hz monitors over USB-C DisplayPort. My keyboard is a HHKB Pro 2.&lt;/p&gt;

&lt;p&gt;Estimated default latencies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Input latency (ms)&lt;/th&gt;
&lt;th&gt;Response time (ms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LG 27UL500-W&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HHKB Pro 2&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I have no way to verify the above numbers, they’re based on a couple sources online, links below. It takes 16ms to draw a full frame on a 4k 60hz display from top to bottom, and things in the exact vertical middle of the display update in 8ms.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.lg.com/us/monitors/lg-27UL500-W"&gt;LG 27UL500-W&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://danluu.com/keyboard-latency/"&gt;Keyboard latency - danluu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I bypassed my USB hub and connected my keyboard directly to the MBP with no discernible difference in results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apps tested
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Terminal emulators
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Terminal.app 2.12.7&lt;/li&gt;
&lt;li&gt;WezTerm 20220624&lt;/li&gt;
&lt;li&gt;Alacritty 0.10.1&lt;/li&gt;
&lt;li&gt;iTerm2 3.4.16&lt;/li&gt;
&lt;li&gt;kitty 0.25.2&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Swordfish90/cool-retro-term"&gt;cool-retro-term&lt;/a&gt; - I threw this one in for kicks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Text editors
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sublime Text 4126&lt;/li&gt;
&lt;li&gt;VS Code 1.68.1&lt;/li&gt;
&lt;li&gt;Typora 1.3.7&lt;/li&gt;
&lt;li&gt;Vim 9.0&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Terminals
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/07/terminal-latency/latency-terminals.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xk-kucs_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/07/terminal-latency/latency-terminals_hue2e161672031f6a252112ea348f2f9ee_48340_1300x0_resize_q85_bgffffff_box_3.jpg" alt="Bar chart showing input latency from least to greatest" width="880" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Terminal&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency (ms)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;kitty - unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;29.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;kitty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;37.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terminal.app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;45.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WezTerm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;45.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;alacritty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;57.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iTerm2 GPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;62.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iTerm2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;87.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cool-retro-term&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;125.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Editors
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/07/terminal-latency/latency-editors.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NNVlutg3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/07/terminal-latency/latency-editors_hu87bf74819768a262eca4d37f5bc9283f_34977_1300x0_resize_q85_bgffffff_box_3.jpg" alt="Bar chart showing input latency from least to greatest" width="880" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;App&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency (ms)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sublime Text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;29.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;62.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typora&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;66.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Combined
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/07/terminal-latency/latency-combined.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pvHsxIc1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/07/terminal-latency/latency-combined_hu76b990de30e4b7c2fc87343fc8f4cf43_61133_1300x0_resize_q85_bgffffff_box_3.jpg" alt="Bar chart showing input latency from least to greatest" width="880" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;App&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency (ms)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sublime Text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;29.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;kitty - unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;29.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;kitty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;37.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terminal.app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;45.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WezTerm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;45.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;alacritty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;57.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;62.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iTerm2 GPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;62.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typora&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;66.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iTerm2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;87.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cool-retro-term&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;125.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;I’m impressed with how fast kitty is, it’s tied with Sublime Text on input latency while running Vim.&lt;/p&gt;

&lt;p&gt;iTerm2’s GPU latency was something I was curious about, as most of the benchmarks I found made no mention of it. While it’s behind the rest of the pack, its features are strongly growing on me. It’s a lot more user friendly and works better on macOS than the others. The keyboard shortcut to restore a saved window layout is so good, &lt;a href="https://iterm2.com/documentation-copymode.html"&gt;Copy Mode&lt;/a&gt; is convenient, and there’s a minimum contrast setting for colors that I wish more terminals had. And there’s a visual drag-and-drop interface for panes so I don’t have to remember the arcane key combo on other terminals.&lt;/p&gt;

&lt;p&gt;WezTerm was the first alternative I settled on – for a few days. It supports font ligatures and extra glyphs out of the box, which enables you to use any font you like. It supports panes, and has its own version of iTerm2’s Copy Mode. And it’s fast, scoring just behind kitty and on par with Terminal.app in my tests. It has trouble making some URLs clickable though, and you can’t use the mouse to drag out tabs into new windows like Terminal.app and iTerm2. Definitely one to keep an eye on.&lt;/p&gt;

&lt;p&gt;Alacritty is meant to be used with a multiplexer like tmux for tabs and panes, and opening new windows means more Alacritty icons in your dock. Fonts are thick on macOS, and it seems the config setting to fix that has no effect. Latency in my tests was roughly 5ms less than iTerm2, so if given the &lt;a href="https://en.wikipedia.org/wiki/False_dilemma"&gt;false dilemma&lt;/a&gt;, I’d use iTerm2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision
&lt;/h3&gt;

&lt;p&gt;I’m going to use iTerm2 for a week and see how it goes. Here’s a pic of the family running &lt;a href="https://visidata.org"&gt;VisiData&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/07/terminal-latency/terminal-family-pic.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---TDwEgnQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/07/terminal-latency/terminal-family-pic_hu17ab944ade06dee4975565b1c25870fb_293667_2048x0_resize_q85_bgffffff_box_3.jpg" alt="Clockwise from top left: Alacritty, WezTerm, iTerm2, Terminal, kitty, cool-retro-term" title="Clockwise from top left: Alacritty, WezTerm, iTerm2, Terminal, kitty, cool-retro-term" width="880" height="484"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Clockwise from top left: Alacritty, WezTerm, iTerm2, Terminal, kitty, cool-retro-term&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thume.ca/2020/05/20/making-a-latency-tester/"&gt;Measuring keyboard-to-photon latency with a light sensor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pavelfatin.com/typing-with-pleasure/"&gt;Typing with pleasure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://danluu.com/term-latency/"&gt;Terminal latency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://danluu.com/input-lag/"&gt;Computer latency: 1977-2017&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Coding fonts comparison</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Mon, 16 May 2022 04:07:23 +0000</pubDate>
      <link>https://dev.to/lkhrs/coding-fonts-comparison-4gni</link>
      <guid>https://dev.to/lkhrs/coding-fonts-comparison-4gni</guid>
      <description>&lt;h2&gt;
  
  
  &lt;a href="https://coding-fonts.pages.dev"&gt;Coding Fonts&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Following CSS Tricks’s &lt;a href="https://www.digitalocean.com/blog/css-tricks-joins-digitalocean"&gt;acquisition&lt;/a&gt; by Digital Ocean, it seems some projects on their domain have had their DNS entries removed, including their handy Coding Fonts comparison site.&lt;/p&gt;

&lt;p&gt;Fortunately, the site’s &lt;a href="https://github.com/chriscoyier/coding-fonts"&gt;source&lt;/a&gt; is on GitHub, and I forked it under the MIT license and put it up on CloudFlare Pages. You can view the new site &lt;a href="https://coding-fonts.pages.dev"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While the code is under the MIT license, the CSS Tricks logo and brand is not, so I removed them to avoid potential issues there, while still acknowledging Chris Coyier’s work. I’m maintaining this for my own personal use, but you are free to use it as well, and open PRs with new fonts or fixes.&lt;/p&gt;

&lt;p&gt;Some fonts don’t work, and I think it’s due to missing external stylesheets provided by font sellers for licensing reasons. The non-working fonts include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hasklig&lt;/li&gt;
&lt;li&gt;Monoid&lt;/li&gt;
&lt;li&gt;Iosevka&lt;/li&gt;
&lt;li&gt;JuliaMono&lt;/li&gt;
&lt;li&gt;Menlo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point I will update the site’s packages or rebuild it in Hugo; it currently uses a year-old version of 11ty.&lt;/p&gt;

&lt;h2&gt;
  
  
  My new preferred coding font
&lt;/h2&gt;

&lt;p&gt;I’ve been a religious &lt;a href="https://coding-fonts.pages.dev/fonts/hack/"&gt;Hack&lt;/a&gt; user since it came out in 2015, though I also like &lt;a href="https://coding-fonts.pages.dev/fonts/cascadia-code/"&gt;Cascadia Code&lt;/a&gt;, Menlo, and &lt;a href="https://coding-fonts.pages.dev/fonts/jetbrains-mono/"&gt;JetBrains Mono&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I went hunting for the Coding Fonts site because I wanted to see how &lt;a href="https://coding-fonts.pages.dev/fonts/san-francisco-mono/"&gt;SF Mono&lt;/a&gt; (shipped with macOS) compared with the rest of them, and then I discovered &lt;a href="https://coding-fonts.pages.dev/fonts/recursive/"&gt;Recursive&lt;/a&gt;, which is now my current coding font. I use the Duotone version for fancy Markdown headers.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>WordPress marketshare is "shrinking"</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Wed, 11 May 2022 23:00:33 +0000</pubDate>
      <link>https://dev.to/lkhrs/wordpress-marketshare-is-shrinking-2g7j</link>
      <guid>https://dev.to/lkhrs/wordpress-marketshare-is-shrinking-2g7j</guid>
      <description>&lt;p&gt;&lt;a href="https://joost.blog/wordpress-market-share-shrinking/"&gt;Joost de Valk writes,&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There’s no more denying it: if you look at &lt;a href="https://w3techs.com/technologies/history_overview/content_management/all"&gt;W3Techs&lt;/a&gt; CMS market share numbers, WordPress’ market share is &lt;em&gt;shrinking&lt;/em&gt;, losing 0.4% market share since February.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a sensationalist article based on faulty stats and anecdotes, though I wanted to comment on some of the points.&lt;/p&gt;

&lt;p&gt;As the author notes that the main source for these stats is the now-defunct &lt;a href="https://en.wikipedia.org/wiki/Alexa_Internet"&gt;Alexa&lt;/a&gt;, and he plans to switch to a different source soon. Those same stats also note that WordPress &lt;em&gt;grew&lt;/em&gt; in marketshare by several percentage points before this slight drop.&lt;/p&gt;

&lt;p&gt;Additionally, W3Techs mentions that they &lt;a href="https://w3techs.com/technologies"&gt;don’t count subdomains&lt;/a&gt;, which leaves out any platform that hosts their user’s site on a subdomain, and large orgs that also adopt the subdomain setup. This leaves out every single site without a custom domain that uses WordPress.com, Blogger, Tumblr, Wix, Webflow, and others.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you look at &lt;a href="http://cwvtech.report/"&gt;cwvtech.report&lt;/a&gt; you’ll see that in the last year, sites on Wix and Squarespace on average have improved their site speed more than WordPress sites. WordPress has a performance team now, and it has made some progress. But the reality is that it hasn’t really made big strides yet, and in my opinion, really should.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When comparing WordPress &lt;a href="https://web.dev/vitals/"&gt;Core Web Vitals&lt;/a&gt; (CWV) against silos like Wix and SquareSpace, it’s important to differentiate between self-hosted (WordPress.org) and WordPress.com, which may be difficult to measure. WordPress.com does a lot to improve performance by default, most sites have JetPack features including image optimization, and their server speed is exceptional, with their own page-caching setup. WordPress.org sites do not come with any of these niceties out of the box, and it’s up to the developers or users to take care of optimization. Slow shared hosting and the added bloat of page builder plugins is common with WordPress.org sites, and negatively impacts CWV.&lt;/p&gt;

&lt;p&gt;There have been efforts to improve the performance of self-hosted sites, but a major performance improvement that added WebP image conversion by default was &lt;a href="https://make.wordpress.org/core/2022/04/12/follow-up-on-webp-by-default-proposal/"&gt;recently rejected&lt;/a&gt; by the community, whose principle concern was the storage space required by generating another set of image sizes in a different format. Why is storage space a problem? Cheap, and usually slow, shared hosting.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WordPress’ full site editing project is not done yet. Anecdotally, more and more people are having a hard time deciding how to build their site on WordPress. Wix and Squarespace are simply way simpler tools to build a site. As they improve their SEO tooling, there’s less and less reason to switch over to WordPress.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I fully agree with this one. My client projects for the last 6 months were built using Webflow, as I needed rapid turnaround and low maintenance that WordPress just doesn’t provide. I don’t want to use Webflow, as I can’t easily switch later (although full code/assets export and CMS to .csv is available), but building sites with it is much faster, and the client’s experience managing their content is &lt;em&gt;way&lt;/em&gt; better. I’ve spent a lot of time, and not a small amount of money, making it easier for me to build and maintain WordPress sites over the years, and yet I still opted for Webflow.&lt;/p&gt;

&lt;p&gt;The primary challenge with WordPress for me is hosting, as I have high standards for performance, but I also have to balance performance with cost, both financially and time spent on maintenance. I still haven’t found a WordPress host or hosting setup that is the best balance of both. I recently migrated my sites from Kinsta to WordPress.com, and plan to write about it soon, but it still doesn’t fully meet my needs with pricing (one year up front) and features (no staging). I can’t start a new project and share a staging site with a client while keeping my initial investment minimal.&lt;/p&gt;

&lt;p&gt;I may end up looking into VPSs again and using &lt;a href="https://spinupwp.com"&gt;SpinupWP&lt;/a&gt; in the future.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I think WordPress, for the first time in a decade, is being out-“innovated”. Now I say “innovated” because Squarespace and Wix are not really doing anything that new. They’re just implementing best practices for both site speed and SEO. They are, however, rolling that out for all their users. So all of their users get better and better page speed performance and improved SEO. As a result more and more of their sites are doing well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I read this and was surprised, as the &lt;a href="https://datastudio.google.com/reporting/55bc8fad-44c2-4280-aa0b-5f3f0cd3d2be/page/M6ZPC?params=%7B%22df44%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580ALL%25EE%2580%2580WordPress%25EE%2580%2580Wix%25EE%2580%2580Squarespace%25EE%2580%2580Webflow%22%7D"&gt;CWV report&lt;/a&gt; doesn’t show a large difference in performance, except in Webflow’s case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/05/wordpress-marketshare/FxeK4uPU.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YN6CXNbp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/05/wordpress-marketshare/FxeK4uPU_hu1f38da78f99428e282399415ac39c6f8_179509_1746x0_resize_q85_bgffffff_box_3.jpg" alt="CWV chart for May 2022" width="880" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chart shows the percentage of sites with good CWV, between January 2020 and April 2022:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webflow at 52.4% across 23,127 sites&lt;/li&gt;
&lt;li&gt;Wix at 36.6% across 106,217 sites&lt;/li&gt;
&lt;li&gt;Squarespace at 32.8% across 59,148 sites&lt;/li&gt;
&lt;li&gt;WordPress at 28.7% across 2,149,028 sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why are Wix and Squarespace so close to WordPress, with a fraction of the sites? No matter the sample size, I’d expect a much bigger difference in stats from a fully-managed service like Wix and Squarespace than WordPress’s combined stats across WordPress.com and .org sites. These managed services have a lot of control over their user’s sites and designs, yet they aren’t making any large strides in performance. I would have expected their numbers to be much closer to 80%-90%.&lt;/p&gt;

&lt;p&gt;I honestly expected Webflow to be at the bottom of this chart, as building sites with their tool is less abstracted from the actual code, and Wix/SquareSpace use a highly abstracted editor system with blocks and components, so Wix and SquareSpace have even more control over making their user’s sites faster.&lt;/p&gt;

&lt;p&gt;Going by these stats, WordPress has 616,771 speedy sites with good CWV, while Webflow, Wix, and Squarespace combined have 97,150 speedy sites, which doesn’t mean anything but I thought it was funny.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If WordPress wants to maintain its market share or better yet, grow it, it’ll have to get its act together. That means it should focus on the performance of these sites across the spectrums of site speed and SEO. The Full Site Editing project is simply taking far too long. That’s causing the rest of the platform to lag behind current web trends. Without a drastic change in this approach I think WordPress will continue to lose market share for the next few years.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’m all for better performance and SEO out of the box with WordPress. I want WordPress to get to the point where I don’t need to install the author’s bloated Yoast plugin or SEO Framework just to fix WordPress SEO issues. Even WordPress.com isn’t immune to the same issues and yet they tout their SEO on their sales pages. Both WP.com and .org create useless attachment pages for every media file uploaded, which clutters the sitemap and may lower your ranking.&lt;/p&gt;

&lt;p&gt;Full Site Editing would be great once polished, and if WP.com offered more features geared towards freelancers and agencies, like staging and monthly billing for hosting, this would be the ideal setup for me over Webflow.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Mastodon app permissions are poorly explained</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Thu, 28 Apr 2022 16:20:32 +0000</pubDate>
      <link>https://dev.to/lkhrs/mastodon-app-permissions-are-poorly-explained-26a9</link>
      <guid>https://dev.to/lkhrs/mastodon-app-permissions-are-poorly-explained-26a9</guid>
      <description>&lt;p&gt;Imagine you’re a new user to &lt;a href="https://joinmastodon.org"&gt;Mastodon&lt;/a&gt;, the federated alternative to Twitter some of your friends are joining. You’re not the most technical person but you love checking out new stuff. You download the official app, or a third party app because your friends are big fans of the local timeline. Then you choose an instance, log in, and are presented with this page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/mastodon-oauth/Lb4dkPE6.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--C10rRqGA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/mastodon-oauth/Lb4dkPE6_hue184e5be2e8ade7b694fd63a0ded3b4b_56298_980x0_resize_q85_h2_box_3.webp" alt="Mastdon app permissions page" width="880" height="1034"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does any of this mean?
&lt;/h2&gt;

&lt;p&gt;None of these permissions are explained. There’s a green checkbox, and apparently the app has read and write access to these things, but what do these things do?&lt;/p&gt;

&lt;p&gt;“Everything” sounds scary, can this app change my password or otherwise take over my account?&lt;/p&gt;

&lt;p&gt;“Relationships” means the app can manage your following list or followers, or does it? Maybe it can also access my contacts on my device?&lt;/p&gt;

&lt;p&gt;“Push notifications” is obvious I guess, but read and write? In this scenario I’m not a technical user, so is this app snooping on my notifications?&lt;/p&gt;

&lt;p&gt;Oh, and “push notifications” is cut off at the bottom, implying that the list is scrollable or expandable, but it isn’t.&lt;/p&gt;

&lt;p&gt;At the top of the page, buried in text, there’s a helpful note that says, “if you do not trust it, then you should not authorize it”. It’s not obvious and is going to get skipped over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is this a problem?
&lt;/h2&gt;

&lt;p&gt;Users should clearly understand what it means to authorize an app to their Mastodon account, with each permission clearly outlined and explained. They’re not going to check the docs or another page to try and figure it out. &lt;strong&gt;The terms used in &lt;a href="https://docs.joinmastodon.org/api/oauth-scopes/#list-of-scopes"&gt;the docs&lt;/a&gt; don’t even match what’s on the authorization screen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I think this is even more important for a “decentralized” network and open API like Mastodon. With many different third-parties hosting instances and creating apps, it should be &lt;em&gt;very clear&lt;/em&gt; what you’re allowing the app to do.&lt;/p&gt;

&lt;p&gt;For contrast, here’s what Twitter does:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/mastodon-oauth/HLmjKeUD.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2kwP1wKg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/mastodon-oauth/HLmjKeUD_huba1e8c8ab347c5b606f3e2de5e7b512c_139677_1314x0_resize_q85_h2_box_3.webp" alt="Twitter OAuth page" width="880" height="959"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page layout really needs some work, but the permissions are explained in detail - &lt;strong&gt;you know exactly what the app can do&lt;/strong&gt; when you authorize it.&lt;/p&gt;

&lt;p&gt;Here’s GitHub’s app authorization page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/mastodon-oauth/3SaLP.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZcCNXljb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/mastodon-oauth/3SaLP_hu19a0e6323efce1a31aa3238cc96433a7_34556_1118x0_resize_q85_h2_box_3.webp" alt="GitHub&amp;amp;rsquo;s OAuth page" width="880" height="960"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neat, I know that this app can find out what my email address is, it was created 3 days ago, and isn’t used by a lot of people.&lt;/p&gt;

&lt;p&gt;And here’s Google’s app authorization page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/mastodon-oauth/cjy8jLQJ.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fIdK1lMl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/mastodon-oauth/cjy8jLQJ_huc74b0c8efc09bfdf2da7ee975bb0f213_113054_1170x0_resize_q85_h2_box_3.webp" alt="Google OAuth page" width="880" height="1139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I like this one a lot because you can control what the app can access. “Personal info” needs to be explained better, but I do know the app can see my email address and can access my Analytics and Search Console data.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I suggest instead
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The “If you do not trust it” notice should be displayed by itself and prominent&lt;/li&gt;
&lt;li&gt;Permissions labels should be more descriptive of what they do&lt;/li&gt;
&lt;li&gt;Change the permission block names to better highlight what they do (Post toots on your behalf, change who you follow)&lt;/li&gt;
&lt;li&gt;List the most destructive action first followed by the next most destructive option (read and post toots and direct messages, add/remove favorites)&lt;/li&gt;
&lt;li&gt;Expandable accordions to list all permissions in the group&lt;/li&gt;
&lt;li&gt;Authorize/deny buttons should always be at the bottom of the list so users have to read the permissions before authorizing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional:&lt;/strong&gt; list of what the app &lt;strong&gt;cannot&lt;/strong&gt; do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/mastodon-oauth/image-20220428130820292.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ox7w8Kx9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/mastodon-oauth/image-20220428130820292_hud90e2c82e560f55b40f252daed374d24_50627_860x0_resize_q85_h2_box_3.webp" alt="Mastodon OAuth page mockup" width="860" height="1682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was a quick mockup I made in Figma. The ordering and specific labeling needs discussion, I’m just illustrating an example that I think better explains what authorizing an app does.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to block domains from search results</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Wed, 13 Apr 2022 21:24:39 +0000</pubDate>
      <link>https://dev.to/lkhrs/how-to-block-domains-from-search-results-1mg3</link>
      <guid>https://dev.to/lkhrs/how-to-block-domains-from-search-results-1mg3</guid>
      <description>&lt;h2&gt;
  
  
  Search results are not helpful
&lt;/h2&gt;

&lt;p&gt;Searching for things on the web has steadily gotten worse&lt;sup id="fnref:1"&gt;1&lt;/sup&gt;&lt;sup id="fnref:2"&gt;2&lt;/sup&gt;. Sites with so-called “content strategies” fill the results. And each article is a poorly researched pile of garbage, haphazardly copied from more legitimate sites.&lt;/p&gt;

&lt;p&gt;Some of these “articles” even seem to be researched off of &lt;em&gt;each other&lt;/em&gt;, rewritten in odd ways to avoid Google’s plagiarism penalty, like a game of telephone. This is also a side effect from using AI writers, which are trained on existing search results.&lt;/p&gt;

&lt;p&gt;And there’s bad, ad-filled mirrors of Stack Overflow, GitHub issues, NPM packages, and Reddit threads, that somehow rank higher than the actual source.&lt;/p&gt;

&lt;p&gt;I’m not going to name any of these sites because I don’t want to boost their rank any further, but I’m sure you’ve seen them.&lt;/p&gt;

&lt;p&gt;Fed up with the SEO spam, I’ve found some ways to make my searching more productive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter your results
&lt;/h2&gt;

&lt;p&gt;I’ve been blocking entire domains from my results. It’s tedious, and my list grows every time I search, but my searches have been a lot more productive. You can do this with a browser extension, a different search engine, or by editing your search query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser extensions
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Let’s Block It!
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://letsblock.it"&gt;Let’s Block It!&lt;/a&gt; is a handy site to generate content filters for uBlock Origin, and you can create an account and share your list, which can be added just like any other uBlock Origin list.&lt;/p&gt;

&lt;p&gt;You’ll want to use &lt;a href="https://letsblock.it/filters/search-results"&gt;their tool&lt;/a&gt; to make your own search results filter, which provides the option to easily add in some exhaustive filters from the &lt;a href="https://github.com/quenhus/uBlock-Origin-dev-filter"&gt;uBlock-Origin-dev-filter&lt;/a&gt; project.&lt;/p&gt;

&lt;p&gt;The filters support result filtering on Bing, DuckDuckGo, Google, Kagi, Searx, and Startpage.&lt;/p&gt;

&lt;p&gt;They also provide a handy &lt;a href="https://github.com/xvello/letsblockit/tree/main/cmd/render"&gt;CLI utility&lt;/a&gt; so you can generate and self-host your lists without using their website.&lt;/p&gt;

&lt;p&gt;Unfortunately, it doesn’t have a quick way to block sites straight from the results, and I’m a filthy Safari user, so uBlock Origin isn’t an option for me.&lt;/p&gt;

&lt;h4&gt;
  
  
  uBlacklist
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/iorate/ublacklist"&gt;uBlacklist&lt;/a&gt; is what I’m currently using. It supports Chrome, Firefox, and Safari, and you can subscribe to &lt;a href="https://iorate.github.io/ublacklist/subscriptions"&gt;other user’s blacklists&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When installed, you get a “Block this site” link under each of your results, like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/block-domains-from-search/9Sct9LgC.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lwjny3bq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/block-domains-from-search/9Sct9LgC_huf16e83f88774d6795ae4032c1702236f_38439_1548x0_resize_q85_h2_box_3.webp" alt="uBlacklist screenshot" width="880" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can easily export your blacklist to a text file - &lt;a href="https://gist.github.com/lkhrs/389c14b7005fca70cc633fb2fa8a66c6"&gt;here’s mine&lt;/a&gt; currently. And if you use Google Drive or Dropbox (I don’t), you can sync your blacklist straight from the extension.&lt;/p&gt;

&lt;p&gt;The extension supports Bing, DuckDuckGo, Google, and Startpage, with &lt;a href="https://iorate.github.io/ublacklist/docs/advanced-features#ecosia"&gt;partial support for Ecosia and Qwant&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit your search query
&lt;/h3&gt;

&lt;p&gt;If you can’t install extensions, and alternative search engines are blocked, bookmarking a query with a bunch of unwanted domains might be your next best option.&lt;/p&gt;

&lt;p&gt;You can add &lt;code&gt;-site:example.com&lt;/code&gt; to most search engines, and just keep adding more &lt;code&gt;-site:&lt;/code&gt; parameters as needed. Then before you put in your search term, bookmark the page, and you’ll be able to reuse it in the future.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/block-domains-from-search/P38CImIH.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3yK2gVsU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/block-domains-from-search/P38CImIH_hu77bbf8a7b895bcd65dc09e4219a208ee_28271_1514x0_resize_q85_h2_box_3.webp" alt="DuckDuckGo with sites manually removed" width="880" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also edit your browser’s default search query&lt;sup id="fnref:3"&gt;3&lt;/sup&gt;. Here’s an example with DuckDuckGo in Brave (note the space between &lt;code&gt;%s&lt;/code&gt; and &lt;code&gt;-site&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/block-domains-from-search/N9MAAP7B.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TiXrRDBA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/block-domains-from-search/N9MAAP7B_hucf5a7dfa8140d1ff63958aaadbfe2d29_28461_996x0_resize_q85_h2_box_3.webp" alt="Custom search engine settings" width="880" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternative search engines
&lt;/h3&gt;

&lt;h4&gt;
  
  
  MetaGer
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://metager.org"&gt;MetaGer&lt;/a&gt; is the only publicly available search engine I’ve found that lets you set a domain blacklist. You can filter results straight from the page, but those filters are temporary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/block-domains-from-search/k3WeinZ9.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--al9CHD5W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/block-domains-from-search/k3WeinZ9_hu0da3e4c3d7058b3339fc52df3082356a_92014_1608x0_resize_q85_h2_box_3.webp" alt="MetaGer search results" width="880" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, MetaGer has a configurable domain blacklist in settings, and they’ll give you a URL with all your settings in it that you can use to &lt;strong&gt;easily copy your settings to other browsers or devices, without an account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/block-domains-from-search/8uBcrIJf.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jQyRb3JP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/block-domains-from-search/8uBcrIJf_hufc9ab98af235344737f6d3380d4560f0_53927_2068x0_resize_q85_h2_box_3.webp" alt="MetaGer blacklist settings" width="880" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/block-domains-from-search/h5gIIXPr.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8k4bgY__--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/block-domains-from-search/h5gIIXPr_hu35f8b5147673cc31b0dc8cb3b306c75f_62510_2056x0_resize_q85_h2_box_3.webp" alt="MetaGer personal settings URL" width="880" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Other search engines
&lt;/h4&gt;

&lt;p&gt;It looks like &lt;a href="https://kagi.com"&gt;Kagi&lt;/a&gt; supports domain blocking, but it’s invite-only and I haven’t been able to test it for myself. I’m also not too keen on paying for a search engine eventually, but if it makes my life easier, sure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-hosted options
&lt;/h3&gt;

&lt;p&gt;The only self-hosted search engine I could find that supports blacklists is &lt;a href="https://yacy.net"&gt;YaCy&lt;/a&gt;, but it requires Java (ugh). The wiki page on the &lt;a href="https://wiki.yacy.net/index.php/De:Blacklists"&gt;blacklists feature&lt;/a&gt; is in German.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/searx/searx"&gt;Searx&lt;/a&gt; or its fork, &lt;a href="https://github.com/searxng/searxng"&gt;SearxNG&lt;/a&gt;, might be another option, but either it doesn’t have a blacklist feature or it’s not documented. I wasn’t able to install it and find out for myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use additional search terms
&lt;/h2&gt;

&lt;p&gt;It’s impossible to find a legitimate review on something outside of a major media site, and I can’t always depend on those reviews either. &lt;strong&gt;I’ve been adding “reddit”&lt;/strong&gt; to a lot of my search terms to try and get crowdsourced opinions. This isn’t ideal because of their slow website and dark patterns, but I do get better results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/greentfrapp"&gt;Lim Swee Kiat&lt;/a&gt; recently made a site that will do this for you, called &lt;a href="https://redditle.com"&gt;Redditle&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can search engines fix the problem?
&lt;/h2&gt;

&lt;p&gt;A better question is, &lt;strong&gt;what’s their incentive to fix this?&lt;/strong&gt; If all the big search engines show the same garbage results (and they do, in my experience), why spend resources making your engine different? Especially when it makes you money because you also own the ads on the low quality sites?&lt;/p&gt;

&lt;p&gt;The answer is, there is no incentive, at least none that I’m aware of for the current problem. It’s up to us, the users, to fix it ourselves.&lt;/p&gt;

&lt;p&gt;That said, here are some ideas I had:&lt;/p&gt;

&lt;h3&gt;
  
  
  User blocklists that influence the algorithm
&lt;/h3&gt;

&lt;p&gt;Google &lt;a href="https://googlesystem.blogspot.com/2011/02/block-domains-from-googles-search.html"&gt;used to have&lt;/a&gt; a Chrome extension that allowed you to block domains directly from the search results list. While they insisted it didn’t have a direct effect on the algorithm, it did report your list of blocked domains to Google, so I like to think it at least had &lt;em&gt;some&lt;/em&gt; effect. But like most Google products, &lt;a href="https://googlesystem.blogspot.com/2013/03/google-discontinues-blocked-sites.html"&gt;they killed it&lt;/a&gt; a few years later in 2013.&lt;/p&gt;

&lt;p&gt;Bring it back, and then weigh the results to prevent abuse. I’m sure there’s a cool techie term for it, but basically, only remove domains if the amount of users blocking the domain reaches a certain percentage of its clicks from results.&lt;/p&gt;

&lt;p&gt;Alternatively, take it a step further and &lt;strong&gt;implement up/down votes&lt;/strong&gt; , similar to link aggregators.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expand the results review team
&lt;/h3&gt;

&lt;p&gt;I’m sure both Google and Bing have a team of people whose sole job is to search for things and test the results. I think this team should be dramatically expanded, and could even be outsourced as an invite-only program to the users if you want to cut costs.&lt;/p&gt;

&lt;p&gt;Both of these ideas will need some abuse protections put in place, so people can’t game it to downrank their competitors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improve the algorithm to better detect low quality content
&lt;/h3&gt;

&lt;p&gt;While knowing absolutely nothing about developing algorithms, I can definitely say this would be extremely difficult to pull off and would only be suspect to more exploitation once the spammers figure it out.&lt;/p&gt;

&lt;p&gt;Most of these SEO spammers use content farms and AI writers, both of which create content Google deems “high quality”, with images, frequent use of headings, and related keywords integrated into the text. And it’s all perfectly readable, often with better grammar skills than I will ever have.&lt;/p&gt;

&lt;p&gt;The AI writers can integrate with your existing SEO tools, and outline a basic structure based on your targeted keywords with headings and even the intro all pre-written for you. Then as you write, the AI will suggest sentences or entire paragraphs based on the preceding heading. I was amazed while testing this out on a short-lived SEO spam experiment of my own last year.&lt;/p&gt;

&lt;p&gt;I have no idea how you would combat this with algorithm updates, because you’d need specific knowledge on the subject of the content to tell the difference between a casual researcher/AI writer and legitimate content.&lt;/p&gt;

&lt;p&gt;Maybe it should be based on your site’s actual subject. For example, if you’re a certain purple-themed WordPress host, maybe you shouldn’t be ranking well with articles about how to switch your default search engine.&lt;/p&gt;

&lt;p&gt;Plagiarism is a lot easier to detect though. I’m not sure why all these bad mirrors of big sites haven’t been penalized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send me more ideas
&lt;/h2&gt;

&lt;p&gt;If you have any ideas on additional browser extensions, search engines, or self-hosted options, please send them my way using the “Reply via email” button below and I’ll update this post.&lt;/p&gt;




&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://news.ycombinator.com/item?id=30031672" rel="noopener"&gt;https://news.ycombinator.com/item?id=30031672&lt;/a&gt; ↩︎&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://www.surgehq.ai//blog/google-search-is-falling-behind" rel="noopener"&gt;https://www.surgehq.ai//blog/google-search-is-falling-behind&lt;/a&gt; ↩︎&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;I thought there was a utility to do this, but I couldn’t find one. Another idea for the backlog! ↩︎&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>How to make static site publishing easier</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Tue, 05 Apr 2022 16:16:34 +0000</pubDate>
      <link>https://dev.to/lkhrs/how-to-make-static-site-publishing-easier-4bi7</link>
      <guid>https://dev.to/lkhrs/how-to-make-static-site-publishing-easier-4bi7</guid>
      <description>&lt;p&gt;So you just got your static site up and running, and if you’re like me, you spent way too much time making your own custom design and other niceties.&lt;/p&gt;

&lt;p&gt;After you’ve published your obligatory “I switched from X to Y!” post that tons of blogs use as their only content for years, you’re ready to start writing for real!&lt;/p&gt;

&lt;p&gt;You decided to go static for one or a combination of the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You love writing in Markdown or HTML&lt;/li&gt;
&lt;li&gt;You love using your preferred text editor&lt;/li&gt;
&lt;li&gt;You’d like to keep your content in a portable format for longevity&lt;/li&gt;
&lt;li&gt;You live in the American Gardens building on West 81st street&lt;/li&gt;
&lt;li&gt;You’re a big fan of using version control&lt;/li&gt;
&lt;li&gt;Paying for a blogging service or hosting is either not ideal or the server you have would struggle with memory-hogging database servers and caches&lt;/li&gt;
&lt;li&gt;You love optimizing and tinkering with things&lt;/li&gt;
&lt;li&gt;It’s a shiny new toy&lt;/li&gt;
&lt;li&gt;You’re not a photographer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m only half kidding on that last bit, handling images is definitely a major Pain Point with static sites.&lt;/p&gt;

&lt;p&gt;Before I go into problems I’ve encountered and how I solved them, let’s wind up, rotate hip, extend shoulder, and knock the obvious catch-all solution right back into the undersea internet volcano it crawled out of:&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a CMS is not a good option
&lt;/h2&gt;

&lt;p&gt;All of the pain points I’m going to outline below can be solved by using a content management system (CMS).&lt;/p&gt;

&lt;p&gt;If you’ve switched your site to static because you didn’t want to pay for hosting, or reduce server overhead, using a CMS defeats the purpose.&lt;/p&gt;

&lt;p&gt;You’re either paying a startup that might not be around in a few years, or you’re using a self-hosted solution that adds overhead, complexity, and security considerations.&lt;/p&gt;

&lt;p&gt;And while a CMS is supposed to make it easier for you to publish, the initial integration can be difficult and complex, and not migration-friendly.&lt;/p&gt;

&lt;p&gt;Images and content might live outside your repo (API solutions), which is bad for longevity. And there’s few CMSs that work with static files and commit to git. And even if they do commit to git, sometimes they add clutter.&lt;/p&gt;

&lt;p&gt;Using a CMS with a static site only makes sense if you’re leveraging the huge traffic handling ability of a static site and need multiple people to manage content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’m talking to myself here: hundreds of thousands of people aren’t going to visit your little personal blog on a daily basis, and you’re not hiring an editor.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There’s no need to over-engineer this, as much fun as that is. Cross that bridge when you get there.&lt;/p&gt;

&lt;p&gt;If you prefer to use a CMS, use a popular, &lt;strong&gt;well-supported&lt;/strong&gt; one-stop-shop CMS, like WordPress, Ghost, or any number of alternatives.&lt;/p&gt;

&lt;p&gt;A note: I’m going to mention “longevity” a lot. To me, longevity in terms of a site’s content means that for at least 5 years, it’s able to be &lt;em&gt;easily&lt;/em&gt; adapted to a new site design or migrated to another system, and not depend on a third-party service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static site pain points and how I solved them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Starting a new post needs preparation
&lt;/h3&gt;

&lt;p&gt;Not including the initial site clone, possible dependency install, and build error wrangling, each post in a static site needs some sort of preparation.&lt;/p&gt;

&lt;p&gt;You’ve got to create the file, and if you’ve got a folder structure like mine, that involves creating a new folder too.&lt;/p&gt;

&lt;p&gt;If you’re using an SSG, you’ve got to put in frontmatter to tell the SSG what the title of the post is, date, and optional tags, description, cover image, draft status, layout options, etc.&lt;/p&gt;

&lt;p&gt;If you’re using straight HTML, you’ve got to add your HTML structure if you want styling (you do, full-width text is awful to read) and maybe navigation links.&lt;/p&gt;

&lt;p&gt;And then you can start writing.&lt;/p&gt;

&lt;p&gt;It doesn’t sound like a lot, but when you have to do it &lt;em&gt;every time&lt;/em&gt;, it quickly becomes a chore.&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution: Use a template, and then automate it
&lt;/h4&gt;

&lt;p&gt;This is probably a “duh” moment, but have a blank file with your frontmatter or HTML already in place, that you can just duplicate and start writing in. This is also referred to as “scaffolding”.&lt;/p&gt;

&lt;p&gt;And keep new post creation to one (1) command or action. Don’t take multiple steps to create the folder, copy the template over, and open it in your editor.&lt;/p&gt;

&lt;p&gt;Write a script, or even just use the bash &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; operator and string multiple commands together and save it in a note and your terminal history.&lt;/p&gt;

&lt;p&gt;You can even automate the post title and date creation.&lt;/p&gt;

&lt;p&gt;The SSG I use on this site, Hugo, has a built-in scaffolding system called &lt;a href="https://gohugo.io/content-management/archetypes/"&gt;Archetypes&lt;/a&gt;, and to create this post, I just typed one command in my project root:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hugo new blog/2022/04/easier-static-site-publishing/index.md&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This creates new folders if they’re not there, creates the Markdown file with the frontmatter, including title and date from the command, and even opens it in my text editor, ready to write.&lt;/p&gt;

&lt;p&gt;You can tell Hugo what your text editor is in your &lt;a href="https://gohugo.io/getting-started/configuration/#newcontenteditor"&gt;config file&lt;/a&gt;, here’s mine for example (YAML):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1newContentEditor: open -a typora

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

&lt;/div&gt;



&lt;p&gt;You may also have to add the command to your security configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1security:
2 exec:
3 allow:
4 - ^subl$
5 - ^open$

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding images is a pain
&lt;/h3&gt;

&lt;p&gt;I love writing in Markdown, but adding images is annoying. Most solutions I’ve seen involve uploading to a third party service and then pasting the URL in your Markdown, which means cost and broken image URLs later that you might have to manually fix across hundreds of posts. Third-party image hosts aren’t a good solution.&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution #1: Use a Markdown editor
&lt;/h4&gt;

&lt;p&gt;With a good Markdown editor, you can just drop the images straight into your post, like you would with a CMS, and it will copy the image files where they need to go.&lt;/p&gt;

&lt;p&gt;I personally use &lt;a href="https://typora.io"&gt;Typora&lt;/a&gt; for this. My images live in the same folder as the post, which makes it easy to delete all the images and other related files when I delete a post. Typora’s image options panel has a preset option for this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/easier-static-site-publishing/2R7gK4P8.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KJa9EGvx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/easier-static-site-publishing/2R7gK4P8_hu433d257ac2ac37ded3651ad96d93ec9b_389482_1664x0_resize_q85_h2_box_3.webp" alt="Typora&amp;amp;rsquo;s image options" width="880" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also set a custom folder and construct the path how you want it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/04/easier-static-site-publishing/UbmcCXIq.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lKHXHqZ3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/04/easier-static-site-publishing/UbmcCXIq_hu9076aa329970108d82cfb17a8ebd0ff5_44548_1020x0_resize_q85_h2_box_3.webp" alt="More Typora image options showing custom settings" width="880" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the folder isn’t created, Typora will prompt you before it creates it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://support.typora.io/Images/#when-insert-images"&gt;More documentation on this feature is available here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Other things I like about Typora: You can use formatting shortcuts, including my favorite, Command+K, to link selected text with a URL from your clipboard. There’s also &lt;a href="https://packagecontrol.io/packages/Typora%20Markdown%20App%20%28OSX%29"&gt;a neat package for Sublime Text&lt;/a&gt; to open the current Markdown file in Typora.&lt;/p&gt;

&lt;h4&gt;
  
  
  Jim’s solution: Build your own CDN
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://blog.jim-nielsen.com/2022/automate-public-folder-workflow/"&gt;Jim Nielsen recently wrote about his setup for easily uploading and adding images to his posts&lt;/a&gt;, where he puts them in a folder automatically synced to Netlify, and then uses macOS Shortcuts to be able to right-click and copy the uploaded image URL straight from Finder.&lt;/p&gt;

&lt;p&gt;I like this solution because you have full control of the folder and the images have a known path — if you stop using this method in the future, you can easily update all your posts with a quick find-replace across files, straight from your text editor or terminal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing images is a pain
&lt;/h3&gt;

&lt;p&gt;Most CMSs have a media library where you can easily see all your images and pick them for a post. You can’t really do that with a static site, or can you?&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution: Your OS file manager
&lt;/h4&gt;

&lt;p&gt;Your operating system’s file manager is a great way to view thumbnails of all your images, and you can easily edit them with whatever your preferred photo editor is. Mine is Affinity Photo.&lt;/p&gt;

&lt;p&gt;Keeping all your images in one folder is a good way to see them all in your file manager, however I didn’t like doing this because when I delete a post, the images don’t get deleted with it. This is a problem I have with CMSs, too.&lt;/p&gt;

&lt;p&gt;So I set up my file structure to have individual folders for each post, and then the images + index.md file go in there, along with any other static files I want to link in the post (like text or PDFs). This also makes it easy to diagnose build errors (I had some old corrupted images causing the build to fail).&lt;/p&gt;

&lt;p&gt;But then you’ve got to go folder by folder to see all your images, right? Nope! Depending on your file manager, you can set up a saved search for all images in your posts directory and then you can view them all in one place&lt;sup id="fnref:1"&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimizing images is a pain
&lt;/h3&gt;

&lt;p&gt;You’ve got your images in your post and saved in your repo. Good!&lt;/p&gt;

&lt;p&gt;They’re taking 5 minutes to load and aren’t responsive. Bad!&lt;/p&gt;

&lt;p&gt;So many pain points with images. This was the biggest one for me. I used to run my images through PhotoBulk to resize them, and then run them through ImageOptim to optimize them. Very complicated process and not as good as responsive images + WebP.&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution: Use an SSG with image optimization
&lt;/h4&gt;

&lt;p&gt;Taking care of this depends on your SSG or build tools. I try to not use build tools outside of the SSG as much as possible, to avoid dependencies and my experience with tools like Gulp has not been successful&lt;sup id="fnref:2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;If you’re using an SSG, try to avoid using shortcodes in your Markdown like the plague, especially for images. If you do use shortcodes, you’re going to have a hard time later fixing your markup across hundreds of posts when you switch generators or move to different setup. Unless you’re good at regex (I’m not).&lt;/p&gt;

&lt;p&gt;Hugo has built-in image optimization and a convenient Markdown image hook template you can use to optimize and generate responsive image sets. &lt;a href="https://www.lkhrs.com/blog/2022/03/one-image-two-days/"&gt;I wrote more about this here&lt;/a&gt;, but the gist is to use &lt;a href="https://github.com/danielfdickinson/image-handling-mod-hugo-dfd"&gt;Daniel F. Dickerson’s handy module&lt;/a&gt;. It’s pretty much drop-in-and-go, with a couple config file additions.&lt;/p&gt;

&lt;p&gt;I no longer feel the need resize and run my original images through ImageOptim, as Hugo does it all for me, and only outputs WebP, so the user never sees the original gigantic image. I do need to set it up for an optimized JPEG fallback though.&lt;/p&gt;

&lt;p&gt;I switched to Hugo from 11ty because it lacked support for optimizing images linked in Markdown, but it appears &lt;a href="https://github.com/11ty/eleventy-img/issues/90#issuecomment-826037424"&gt;Alexs7zzh figured out how do it with eleventy-img&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whatever your SSG is, it’s most likely not going to be a batteries-included setup, and it could be as simple as adding a dependency or heavily involved, where you’re writing your own solution.&lt;/p&gt;

&lt;h4&gt;
  
  
  Another solution: Use a script to optimize your images
&lt;/h4&gt;

&lt;p&gt;You can build your own optimization script, and run it on deployment. &lt;a href="https://css-tricks.com/converting-and-optimizing-images-from-the-command-line/"&gt;CSS-Tricks has a good article on building an image optimization script&lt;/a&gt;. This would be a great combination with Jim Nielsen’s build-your-own-CDN method, linked above.&lt;/p&gt;

&lt;p&gt;You won’t have responsive images and width/height attributes on Markdown images, which isn’t great, but it’s also better than serving gigantic images.&lt;/p&gt;

&lt;p&gt;I’d use WebP until AVIF support has full browser adoption (probably in about 60 years), and serve a maximum size of 2000x2000 px.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forms and comments
&lt;/h3&gt;

&lt;p&gt;You want &lt;em&gt;dynamic&lt;/em&gt; stuff? On your &lt;em&gt;static&lt;/em&gt; site? What?!&lt;/p&gt;

&lt;p&gt;Some SSGs can act as hybrids, where they can be mostly static with dynamic elements, like Next.js and Eleventy Serverless. This is also referred to as server-side rendering (SSR). I haven’t worked with this before, so I’m not sure how to handle forms using SSR.&lt;/p&gt;

&lt;p&gt;Other SSGs need some extra wiring.&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution #1: Email
&lt;/h4&gt;

&lt;p&gt;It’s the simplest solution, and it works so well, even if you use a one-stop-shop CMS.&lt;/p&gt;

&lt;p&gt;Make a new alias just for handling emails related to the blog, and use it on your site. I recommend routing those emails to a folder in your inbox, in case you get an angry mob or something. Mine goes to “Comments”.&lt;/p&gt;

&lt;p&gt;This can act as both your contact form and comments solution, and I really prefer it over commenting systems and forms. You can even add a pre-filled subject line and email body.&lt;/p&gt;

&lt;p&gt;There’s 0 spam (&lt;a href="https://support.cloudflare.com/hc/en-us/articles/200170016-What-is-Email-Address-Obfuscation-"&gt;thanks CloudFlare&lt;/a&gt;) and 0 moderating. If you do need to moderate, just block the sender, right from your inbox. And I love the one-on-one penpal feeling, in a time when casual emails are a rarity.&lt;/p&gt;

&lt;p&gt;For your contact page, spell out the full email address instead of hiding it behind a button — sometimes the user can’t use their default mail client.&lt;/p&gt;

&lt;p&gt;For your posts, I wrote more about &lt;a href="https://www.lkhrs.com/blog/2021/07/alternative-to-comments/"&gt;adding a “reply via email” button here&lt;/a&gt; for 11ty. On this Hugo site, I use &lt;a href="https://www.brycewray.com"&gt;Bryce Wray’s&lt;/a&gt; &lt;a href="https://github.com/lkhrs/hugo-cactus-theme/blob/5dc7eb2e2bf2dd809ff1cc6e194018701c73d282/layouts/blog/single.html#L92"&gt;code&lt;/a&gt;, and &lt;a href="https://kevq.uk/adding-the-post-title-to-my-reply-by-email-button/"&gt;Kev Quirk has a guide for WordPress&lt;/a&gt; (had to mention it even though I’m talking about SSGs).&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution #2: HTML forms and third-party handler
&lt;/h4&gt;

&lt;p&gt;Sometimes there’s no way around it, and you absolutely need a form for your static site. The best way to handle that is to write an HTML form, and then use a third-party service that you can just drop in the &lt;code&gt;action&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;I’m still looking for a solution to self-host, maybe using functions on Netlify/Vercel or CloudFlare Workers, but in the meantime, I broke my own rule and use &lt;a href="https://www.formcake.com"&gt;Formcake&lt;/a&gt; for a few sites. Netlify Forms is fine too.&lt;/p&gt;

&lt;p&gt;I’m okay with using a third-party form handler for HTML forms. If it goes down, I can just update the &lt;code&gt;action&lt;/code&gt; attribute to point to a different URL.&lt;/p&gt;

&lt;p&gt;Forms are so prone to spam though, even with reCAPTCHA and CloudFlare’s challenge turned on just for the page with the form. Also, did you know &lt;a href="https://stackoverflow.com/questions/41665935/html5-form-validation-before-recaptchas"&gt;reCAPTCHA breaks HTML form validation&lt;/a&gt;? Now you know.&lt;/p&gt;

&lt;p&gt;I do miss Akismet on WordPress. Caught 99% of spam submissions for me.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile and away-from-home publishing
&lt;/h3&gt;

&lt;p&gt;Not having a CMS is a big disadvantage here. WordPress has a fantastic mobile app, and there are tons of other apps that also post to WordPress or Ghost, including desktop apps like Ulysses and iA Writer. It’s a really solid experience, and there’s nothing like that for static sites.&lt;/p&gt;

&lt;p&gt;I usually don’t publish on mobile unless I absolutely have to, but I do edit posts I’m working on from time to time.&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution for mobile: Working Copy
&lt;/h4&gt;

&lt;p&gt;I use an iPhone, and &lt;a href="https://workingcopyapp.com"&gt;Working Copy&lt;/a&gt; is the best git client I’ve found. The Markdown editor isn’t bad either. You’ll have to manually copy over your new post template though, and if your repo size is large, it could take a while to clone for the first time over a data connection. And you’re back to painfully inserting images by adding them to the folder and then typing out the image names in Markdown.&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution for desktop: GitHub Codespaces
&lt;/h4&gt;

&lt;p&gt;If your repo is on GitHub, you can use GitHub Codespaces, which is VS Code in your browser. Just navigate to your repo and hit . (period) on your keyboard, and it will fire right up.&lt;/p&gt;

&lt;p&gt;If you prefer Vim and have your own git server, you can ssh into your server and use Vim there, though if you have your own git server you probably do that already.&lt;/p&gt;

&lt;h3&gt;
  
  
  Complicated publishing
&lt;/h3&gt;

&lt;p&gt;This is supposed to be the simplest part, right? Deploy your site to a host that automatically runs your build command and hosts your static files, and you’re done. However, I found myself adding unnecessary steps to my workflow.&lt;/p&gt;

&lt;p&gt;I used to use two separate branches in Git: one for writing and testing, &lt;code&gt;dev&lt;/code&gt;, and one for production deployment, &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is what my publishing workflow looked like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checkout branch &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start up live reload dev environment (&lt;code&gt;hugo server -DF&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Write the post and probably tweak the design along the way&lt;/li&gt;
&lt;li&gt;Commit, usually multiple times&lt;/li&gt;
&lt;li&gt;Push to origin&lt;/li&gt;
&lt;li&gt;Wait for my site to build on CloudFlare Pages (2-5 minutes)&lt;/li&gt;
&lt;li&gt;Check the preview URL to make sure things looked right&lt;/li&gt;
&lt;li&gt;Merge &lt;code&gt;dev&lt;/code&gt; with &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Commit&lt;/li&gt;
&lt;li&gt;Push to origin&lt;/li&gt;
&lt;li&gt;Wait for build&lt;/li&gt;
&lt;li&gt;Check production site to make sure things looked right&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a workflow for a large enterprise or software project, not a personal blog.&lt;/p&gt;

&lt;h4&gt;
  
  
  My solution: Write and deploy on main
&lt;/h4&gt;

&lt;p&gt;To simplify my publishing process, I write on &lt;code&gt;main&lt;/code&gt; and design on &lt;code&gt;dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My repo is set up on both CloudFlare Pages (primary) and Vercel (backup), and Vercel sends me helpful emails on whether my build succeeded or failed (should set this up for CFP too), so I rarely check the published article now.&lt;/p&gt;

&lt;p&gt;And if a build fails, it doesn’t take my site down, it just stays at the previous successful commit. But most of my previous build errors were from messing with the design, and I haven’t seen a build error with my current method.&lt;/p&gt;

&lt;p&gt;It does help to use a Markdown editor with live preview and spellcheck.&lt;/p&gt;

&lt;p&gt;This is what my new publishing workflow looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checkout branch &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Write the post&lt;/li&gt;
&lt;li&gt;Commit&lt;/li&gt;
&lt;li&gt;Push to origin&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Much less complicated.&lt;/p&gt;

&lt;p&gt;And if you’re worried about spelling or formatting mistakes, don’t — it’s a personal blog post, not a press release. Mistakes make it seem more authentic, or so I tell myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reduce your barriers to writing
&lt;/h2&gt;

&lt;p&gt;This is a long article to say just this, but it can’t be overstated. This concept can be applied to any form of creation, not just writing.&lt;/p&gt;

&lt;p&gt;The more complexity you face while creating new content, the less you’ll want to create.&lt;/p&gt;

&lt;p&gt;If using a one-stop-shop CMS helps you write and publish more often, don’t waste time using a static site generator. Likewise, if your CMS is complicated, slow, or if you have unstable Internet, an SSG might be better suited.&lt;/p&gt;




&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Currently I’m unable to do so on macOS and I’m not sure why, it’s possible my Spotlight index is broken. It works in my documents but not in my repo. ↩︎&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Learning Gulp is something I should probably do at some point, I’m sure I’ll run across it on another project. ↩︎&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>One Image Two Days: Optimizing images in Hugo</title>
      <dc:creator>Luke Harris</dc:creator>
      <pubDate>Sun, 27 Mar 2022 17:29:32 +0000</pubDate>
      <link>https://dev.to/lkhrs/one-image-two-days-optimizing-images-in-hugo-1g23</link>
      <guid>https://dev.to/lkhrs/one-image-two-days-optimizing-images-in-hugo-1g23</guid>
      <description>&lt;p&gt;I added a hero image to my &lt;a href="https://www.lkhrs.com/"&gt;homepage&lt;/a&gt;, and here’s the breakdown on how long it took me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the image with an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element and style it: &lt;strong&gt;1 minute&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add image optimization to my Hugo templates: &lt;strong&gt;2 days&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Backstory
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/03/lkhrs-version-6/#images"&gt;Previously&lt;/a&gt;, I used Hugo’s &lt;a href="https://gohugo.io/templates/render-hooks/"&gt;&lt;code&gt;render-image&lt;/code&gt; hook&lt;/a&gt; to optimize the images I put in my Markdown files, &lt;a href="https://www.bennettnotes.com/notes/hugo-responsive-images-with-markdown-render-hook/"&gt;with code by Dave Bennett&lt;/a&gt;. This worked well and gave me WebP images with a set of different sizes.&lt;/p&gt;

&lt;p&gt;You can also &lt;a href="https://laurakalbag.com/processing-responsive-images-with-hugo/"&gt;make a custom &lt;code&gt;img&lt;/code&gt; shortcode for this&lt;/a&gt;, however Hugo shortcodes only work in Markdown files and not templates, which is what I needed for my homepage template. This was confusing to me, coming from 11ty and Nunjucks, where you can use shortcodes anywhere.&lt;/p&gt;

&lt;p&gt;Hugo uses &lt;a href="https://gohugo.io/templates/partials/"&gt;partial templates&lt;/a&gt; instead of shortcodes in templates, which can accept arguments or just act as a reusable component. This is similar to &lt;a href="https://gohugo.io/templates/partials/"&gt;Nunjuck’s &lt;code&gt;include&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, I wanted to use the same image processing code for both Markdown files &lt;em&gt;and&lt;/em&gt; templates. Trying to stay &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;DRY&lt;/a&gt; over here (don’t look at &lt;a href="https://github.com/lkhrs/hugo-cactus-theme"&gt;my theme repo&lt;/a&gt; though, it’s very wet).&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;I discovered &lt;a href="https://github.com/danielfdickinson/image-handling-mod-hugo-dfd"&gt;DFD’s excellent image handling module&lt;/a&gt; for Hugo. I encountered some issues, so &lt;a href="https://github.com/lkhrs/image-handling-mod-hugo-dfd"&gt;I forked it&lt;/a&gt; to customize it for my site’s setup.&lt;/p&gt;

&lt;p&gt;Some things I changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removed &lt;a href="https://github.com/danielfdickinson/link-handling-mod-hugo-dfd"&gt;DFD’s link handler module&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;It was adding empty &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags around images, and &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; tags around other links. Will try to narrow this down and open a PR.&lt;/li&gt;
&lt;li&gt;It changes your external links to open in the same tab by default with no config option though, and I prefer new tabs for external links (&lt;code&gt;target="_blank"&lt;/code&gt;). There is a solid case though for the “Don’t Break the Back Button”&lt;sup id="fnref:1"&gt;1&lt;/sup&gt; rule and I’m slowly coming around on this.&lt;/li&gt;
&lt;li&gt;There’s still some code in my fork that depends on the module if you use the link functionality, and I may end up forking the link handler in the future to make it work the way I want it to. The link checking is handy.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element by default instead of &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;This is personal preference, until I add a JPEG fallback&lt;/li&gt;
&lt;li&gt;Removed the &lt;code&gt;src&lt;/code&gt; attribute from the &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; element as this only used for audio/video (&lt;a href="https://html.spec.whatwg.org/multipage/embedded-content.html#the-picture-element"&gt;spec&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Add link to Markdown images so the user can view them full-size in their browser (I don’t plan on adding a lightbox viewer due to the JavaScript weight)

&lt;ul&gt;
&lt;li&gt;This doesn’t work for images from &lt;code&gt;/assets&lt;/code&gt; though&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;My cover image URLs are in the &lt;code&gt;cover:&lt;/code&gt; key, so &lt;a href="https://github.com/lkhrs/image-handling-mod-hugo-dfd/commit/3eecd09f651649c400fa95c8cdad2aa59b41c02b"&gt;I added it to &lt;code&gt;find-featured-images.html&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This resulted in two days of going through confusing Hugo template code across 7 layout files, but I’ve definitely learned a lot about how Hugo templating works.&lt;/p&gt;

&lt;p&gt;I still need to add JPEG fallback to the fork, currently it only outputs one format (WebP) in the source set. &lt;a href="https://caniuse.com/webp"&gt;WebP is supported by all major browsers&lt;/a&gt;, but Safari added support not even two years ago, so it’s best to continue to add a JPEG fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hugo module issues
&lt;/h3&gt;

&lt;p&gt;I also learned a bit about &lt;a href="https://gohugo.io/hugo-modules/"&gt;Hugo modules&lt;/a&gt;. I encountered a lot of difficulty trying to fork the module and then point my Hugo install at my fork, and I still don’t know what worked exactly. It was a combination of deleting &lt;code&gt;go.mod/sum&lt;/code&gt; and liberal use of &lt;code&gt;hugo mod clean&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And I learned that Hugo modules don’t like getting daisy-chained: I tried to add my theme as a module, and then add a config to the theme that has additional modules needed by the theme, and I encountered some weird error with the last module in the list of 3 not being found. I tried to reorder them or remove one to no effect.&lt;/p&gt;

&lt;p&gt;So now my Hugo project folder is set up with all the modules in the main config, and then my theme + forked modules are cloned into the &lt;code&gt;themes&lt;/code&gt; directory. Then I added a &lt;a href="https://gohugo.io/hugo-modules/configuration/#module-config-top-level"&gt;&lt;code&gt;replacements&lt;/code&gt; option&lt;/a&gt; to my &lt;code&gt;development/config.yaml&lt;/code&gt; to tell Hugo to use the local modules when I’m running &lt;code&gt;hugo server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1module:
2 replacements: "github.com/lkhrs/hugo-cactus-theme -&amp;gt; hugo-cactus-theme,github.com/lkhrs/image-handling-mod-hugo-dfd -&amp;gt; image-handling-mod-hugo-dfd"

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

&lt;/div&gt;



&lt;p&gt;All this to avoid using &lt;a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules"&gt;Git Submodules&lt;/a&gt;, which probably would have been easier at this point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I have a partial I can use whenever I need to put an image on a page, though it is pretty complicated. This is the one I’m using for the hero image on the home page:&lt;sup id="fnref:2"&gt;2&lt;/sup&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1{{ partial "helpers/wrapped-image" (dict "alt" "Luke in the shower" "image" (partial "helpers/lib/image-handling/find-image-src" (dict "src" "layouterror.jpg" )) "page" .Page "class" "img-fluid h-25" "noImageWrapper" "true" "loading" "eager") }}
2{{/* We're using loading="eager" instead of "lazy" because the image is above the fold. */}}

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

&lt;/div&gt;



&lt;p&gt;And this is what it looks like in the generated HTML:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lkhrs.com/blog/2022/03/one-image-two-days/pictureElement.jpg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZeoIEIw8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.lkhrs.com/blog/2022/03/one-image-two-days/pictureElement_hu2b365dda1372d52922ddd4bed13a1c92_174716_1519x0_resize_q85_h2_box.webp" alt="Screenshot of picture element in browser dev tools" width="880" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nice optimized WebP images, hurray! And it’s the same logic for rendering Markdown images natively, so I can continue to add images the Markdown way (&lt;code&gt;![alt](imgURL)&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Needless to say, optimizing images is important and one of the first things to look at when you’re trying to speed up a site.&lt;/p&gt;

&lt;p&gt;However, one of the struggles with the whole JAMstack/static site generator (SSG) trend is optimizing images, because very few of the SSGs out there do it for you. And they all need some sort of wrangling to get it working.&lt;/p&gt;

&lt;p&gt;Most image optimization methods I’ve seen for SSGs involve just using a third-party optimization service, and I’m not down with this for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cost - yet another thing I have to pay for and/or keep track of quotas&lt;/li&gt;
&lt;li&gt;Broken image URLs if the service goes down or I have to switch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want your Markdown images optimized, a lot of them want you to use a custom shortcode in your Markdown instead of the Markdown way, which isn’t good for content portability if you change your SSG later.&lt;/p&gt;

&lt;p&gt;This is one of the reasons &lt;a href="https://www.lkhrs.com/blog/2021/12/hugo/"&gt;I switched to Hugo&lt;/a&gt; last year, because of the &lt;code&gt;render-image&lt;/code&gt; hook enables me to optimize images in my posts and not just in templates.&lt;/p&gt;




&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://web.archive.org/web/20050831201716/http://www.useit.com/alertbox/990530.html" rel="noopener"&gt;Top Ten New Mistakes of Web Design (1999)&lt;/a&gt; and &lt;a href="https://www.brightorangethread.com/blog/view/dont-break-the-back-button/" rel="noopener"&gt;Don’t break the back button (2011)&lt;/a&gt; ↩︎&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Can’t wait for the alt text “Luke in the shower” to get copy-pasted into someone’s template. ↩︎&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

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