<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Hugo on Erik Failing</title><link>https://erikfailing.com/tags/hugo/</link><description>Recent content in Hugo on Erik Failing</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sat, 31 Oct 2026 11:01:45 -0500</lastBuildDate><atom:link href="https://erikfailing.com/tags/hugo/index.xml" rel="self" type="application/rss+xml"/><item><title>Building the Color Accents on This Site</title><link>https://erikfailing.com/posts/building-color-accents/</link><pubDate>Fri, 30 Oct 2026 19:30:00 -0500</pubDate><guid>https://erikfailing.com/posts/building-color-accents/</guid><description>Three versions of a text-coloring system, from automatic keyword matching to hand-placed accents, and what each one taught me.</description><content:encoded><![CDATA[<p>The colored words on this site went through three versions before they actually did their job.
I started with a <span class="accent accent-static accent-grad">fully automatic keyword matcher</span>, then made it per-page, and ended up
deleting most of it for something way simpler. This is the builder&rsquo;s side of that, including the
parts that didn&rsquo;t work.</p>
<h2 id="the-goal">The Goal</h2>
<p>I play a lot of ASCII and text-based games, and one of my favorite details in <span class="accent accent-qud accent-grad">Caves of Qud</span> is
how it colors certain words: an item name in a gradient, a faction in its own color. It makes a
wall of text easy to scan. I wanted the same thing here, with a few constraints:</p>
<ul>
<li><strong>Professional, not zany.</strong> It&rsquo;s a site people look at when deciding whether to hire me.</li>
<li><strong>Readable for everyone.</strong> Every color has to pass <span class="accent accent-clarity">WCAG AAA contrast</span> on the dark background,
and screen readers have to read the words normally.</li>
<li><strong>No inline styles.</strong> The site has a strict <span class="accent accent-ward">Content-Security-Policy</span> (<code>style-src 'self'</code>), so
every color has to come from a class in a stylesheet.</li>
</ul>
<h2 id="version-1-automatic-keyword-matching">Version 1: Automatic Keyword Matching</h2>
<p>The first design was a keyword table in a data file, applied automatically at build time. Add
&ldquo;fuzzing&rdquo; to the table, and every page that mentions fuzzing gets it colored. That&rsquo;s
<span class="accent accent-trophy">retroactive and zero effort per post</span>, which sounded great.</p>
<p>The hard part was that Hugo only gives you the rendered HTML, and I had to color text without ever
touching markup. A keyword inside a link, a code block, a heading or an <code>href</code> attribute must stay
alone. Go&rsquo;s regex engine has <span class="accent accent-glitch accent-grad">no lookbehind</span>, so you can&rsquo;t just write &ldquo;match this word unless
it&rsquo;s inside <code>&lt;code&gt;</code>.&rdquo; I ended up splitting each page into tokens (tags vs. plain text), tracking
which protected element I was inside, and only running replacements on plain text:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">findRE</span><span class="w"> </span><span class="s">`&lt;!--[\s\S]*?--&gt;|&lt;[^&gt;]*&gt;|[^&lt;]+`</span><span class="w"> </span><span class="nx">$html</span><span class="w"> </span><span class="cp">-}}</span>
</span></span><span class="line"><span class="cl">  <span class="cp">{{-</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">hasPrefix</span><span class="w"> </span><span class="na">.</span><span class="w"> </span><span class="s">&#34;&lt;&#34;</span><span class="w"> </span><span class="cp">-}}</span>  <span class="cm">{{/* a tag: update which protected element we&#39;re in */}}</span>
</span></span><span class="line"><span class="cl">  <span class="cp">{{-</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="k">eq</span><span class="w"> </span><span class="nx">$protTag</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w"> </span><span class="cp">-}}</span>  <span class="cm">{{/* plain text outside code/links/headings: safe to edit */}}</span>
</span></span></code></pre></div><p>Matches got swapped for private-use placeholder characters first, so a short keyword couldn&rsquo;t
match inside a longer one that was already colored. Then there were the small surprises: Hugo&rsquo;s
typographer turns <code>'</code> into <code>&amp;rsquo;</code>, so &ldquo;Let&rsquo;s Encrypt&rdquo; never matched, and <code>&amp;</code> becomes <code>&amp;amp;</code>.</p>
<p>The gradients had their own constraint. Coloring each letter separately means one <code>&lt;span&gt;</code> per
letter, and current testing shows that <span class="accent accent-ghostly accent-grad">screen readers handle per-letter spans badly</span>
(spelling words out, or reading only the first letter). So every gradient is a single element
with <code>background-clip: text</code>, which paints the gradient inside the glyphs and leaves the text alone.</p>
<p><span class="accent accent-point">It worked, but it colored words, not ideas</span>. Pages about topics on the list got crowded, and
pages about anything else got nothing.</p>
<h2 id="version-2-per-page-picks">Version 2: Per-Page Picks</h2>
<p>The fix seemed obvious: let each page pick its own terms in front matter, with the shared table
kept as a library so &ldquo;fuzzing&rdquo; is the same color everywhere. I added build checks too: the build
<span class="accent accent-ward">fails on a typo&rsquo;d term</span> and warns when a picked term never gets colored.</p>
<p>Then the library grew to about 110 terms, including tools and languages in their brand colors
(Python&rsquo;s blue and yellow, Rust&rsquo;s copper, Claude&rsquo;s terracotta). A lot of brand colors are too dark
for a near-black page, so a script <span class="accent accent-arcane">lightened each one in OKLCH until it passed 7:1</span>. Raising
only the lightness keeps the hue and saturation, so reds stay red instead of fading to pink.</p>
<p>Two problems showed up once real posts used it:</p>
<ol>
<li><strong>Too many gradients.</strong> When everything shimmers, nothing stands out. I cut them to
<span class="accent accent-trophy">about 20% of styles</span> and made the rest solid colors.</li>
<li><strong>First mentions cluster.</strong> An intro paragraph previews the whole post, so it holds the first
mention of half the picked terms and lights up like a Christmas tree.</li>
</ol>
<p>I also shipped a bug here worth admitting: a Hugo pipe passes its value as the <em>last</em> argument, I
wrote it as if it were the first, and <span class="accent accent-glitch accent-grad">every match on the site silently disappeared</span>. A
coverage report I&rsquo;d written for checking density caught it before anything went live.</p>
<h2 id="version-3-just-mark-the-point">Version 3: Just Mark the Point</h2>
<p>Eventually I realized I was solving the wrong problem. A machine can match terms reliably, but it
can&rsquo;t tell which phrase in a paragraph is <em>the point</em>, and that&rsquo;s what I actually wanted readers to
catch. So I deleted the matcher, the library and the front matter picks, and replaced all of it
with <span class="accent accent-point">an inline marker you place by hand</span>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">The {=flow: upload step=} {=glitch: skips anything starting with a dot=} by default.
</span></span><span class="line"><span class="cl">If the build fails, {=point: the live site stays on the last good version=}.
</span></span></code></pre></div><p>There are two tiers. Colored accents mark the tool, company or idea a paragraph is about, picked
from a palette where every style has a short note on when to use it. A key-point style (bright
italics, no color) marks the one takeaway of a section, at most once per section. The
guideline is about <span class="accent accent-clarity">one accent per paragraph</span>, on the phrase that carries the idea.</p>
<p>The syntax took a couple of tries. My first version reused link syntax, <code>[phrase](=style)</code>, with a
render hook, which worked great on the site and <span class="accent accent-crimson">looked like broken links on GitHub</span>. Double
brackets (<code>[[style: phrase]]</code>) never fired, because Markdown&rsquo;s link parser claims <code>[</code> first. What
worked was Hugo&rsquo;s passthrough extension, which hands anything between custom delimiters to a render
hook untouched:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">goldmark</span><span class="p">.</span><span class="nx">extensions</span><span class="p">.</span><span class="nx">passthrough</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">enable</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">goldmark</span><span class="p">.</span><span class="nx">extensions</span><span class="p">.</span><span class="nx">passthrough</span><span class="p">.</span><span class="nx">delimiters</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="nx">inline</span> <span class="p">=</span> <span class="p">[[</span><span class="s1">&#39;{=&#39;</span><span class="p">,</span> <span class="s1">&#39;=}&#39;</span><span class="p">]]</span>
</span></span></code></pre></div><p>I skipped <code>::</code> and <code>&lt;&lt;</code> as delimiters because they show up in normal writing about code
(<code>std::vector</code>, heredocs). The render hook splits off the style name, <span class="accent accent-ward">fails the build on an
unknown style</span>, and runs the inner text back through Markdown so bold and code inside an accent
still work. After converting, I diffed every rendered accent and link against a saved baseline,
and the output was byte-for-byte identical.</p>
<h2 id="what-id-take-from-it">What I&rsquo;d Take From It</h2>
<p>I think the main lesson is to automate what&rsquo;s actually mechanical and leave judgment to people.
Contrast checks, style validation and &ldquo;is this markup well-formed&rdquo; are perfect for the build to
enforce. Picking what matters in a paragraph isn&rsquo;t, and version 1 and 2 were me spending a lot of
clever code avoiding that.</p>
<p>The final system is a palette file, a CSS template that generates one class per style, and two
small render hooks. <span class="accent accent-point">That&rsquo;s less template code than the first version&rsquo;s matcher alone</span>, and
it does the job better. I built it with Claude Code doing a lot of the implementation and testing, which made
it cheap to try (and throw away) the first two versions lol.</p>
]]></content:encoded></item><item><title>How I Built This Site</title><link>https://erikfailing.com/posts/how-i-built-this-site/</link><pubDate>Fri, 30 Oct 2026 17:45:00 -0500</pubDate><guid>https://erikfailing.com/posts/how-i-built-this-site/</guid><description>Hugo, PaperMod, GitHub Actions and a custom domain, with an LLM helping along the way.</description><content:encoded><![CDATA[<p>This site is a static <span class="accent accent-hugo">Hugo</span> site using the PaperMod theme, built and deployed by <span class="accent accent-flow">GitHub Actions</span> to
GitHub Pages, with my Squarespace domain pointed at it. Below is how the pieces fit together, the
design decisions I made along the way, and how I used an LLM to help (and where I had to catch its
mistakes).</p>
<h2 id="the-setup-in-one-picture">The Setup In One Picture</h2>
<p>The whole thing is pretty simple once you see the flow:</p>
<ol>
<li>I write Markdown and push to <code>main</code>.</li>
<li>A GitHub Actions workflow installs Hugo, builds the site, and uploads the result.</li>
<li>GitHub Pages serves it, and my domain points at GitHub Pages.</li>
</ol>
<p>There&rsquo;s no server for me to patch, no database, and no hosting bill. For a personal site I think
that&rsquo;s the right amount of infrastructure: <span class="accent accent-point">the attack surface is basically a folder of HTML files</span>.</p>
<h2 id="why-hugo-and-why-papermod">Why Hugo, And Why PaperMod</h2>
<p>I went with Hugo because it&rsquo;s a <span class="accent accent-iron">single binary</span>, it builds fast (this site currently builds in under
100 ms locally), and the output is plain static files. No Node toolchain to keep updated, which I
was honestly happy to skip.</p>
<p>For the theme, the first plan an LLM helped me brainstorm was actually <span class="accent accent-flood">Blowfish</span>. It looks great,
but when I compared them, PaperMod won for a couple of practical reasons:</p>
<ul>
<li><strong>Readability.</strong> PaperMod&rsquo;s article page is 18px text in a single ~720px column, with a lot less
visual chrome. People who read writeups are the main audience, so <span class="accent accent-point">the article page is what has
to be good</span>.</li>
<li><strong>Build compatibility.</strong> Blowfish needs Hugo&rsquo;s extended edition and capped its supported version
below what the official GitHub Pages workflow installs. PaperMod just works with standard Hugo.</li>
</ul>
<p>The theme is pulled in as a <span class="accent accent-git">Git submodule</span>, so the repo only stores a pointer to one specific
commit of PaperMod instead of a copy of its files:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule add --depth<span class="o">=</span><span class="m">1</span> https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
</span></span></code></pre></div><p>Updating it is a deliberate step (<code>git submodule update --remote</code>, check locally, commit), so a
theme change <span class="accent accent-ward">never lands on the live site by surprise</span>. I also never edit the theme&rsquo;s files. Every
customization lives in my own <code>layouts/</code> and <code>assets/</code> folders, which Hugo uses in place of the
theme&rsquo;s versions.</p>
<h2 id="build-and-deploy-with-github-actions">Build And Deploy With GitHub Actions</h2>
<p>The workflow is <span class="accent accent-flow">Hugo&rsquo;s official GitHub Pages workflow</span>, with two small changes. I pulled it from
Hugo&rsquo;s docs repo directly and diffed my copy against it, so I know exactly what&rsquo;s different:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">HUGO_VERSION</span><span class="p">:</span><span class="w"> </span><span class="m">0.169.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">TZ</span><span class="p">:</span><span class="w"> </span><span class="l">America/Chicago            </span><span class="w"> </span><span class="c"># change 1: my time zone, for dates Hugo prints</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># ...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Upload artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/upload-pages-artifact@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">include-hidden-files</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">   </span><span class="c"># change 2: publish /.well-known/security.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./public</span><span class="w">
</span></span></span></code></pre></div><p>The second one was a fun one to find. The upload step <span class="accent accent-glitch accent-grad">skips anything starting with a dot</span> by
default, which is a sensible default, but it means <code>/.well-known/</code> never gets published. More on
why I wanted that folder below.</p>
<p>A few things I like about this setup:</p>
<ul>
<li>The <span class="accent accent-flow">build job</span> only gets <code>contents: read</code>, <code>pages: write</code> and <code>id-token: write</code>. It can&rsquo;t push
to the repo.</li>
<li>If the build fails, nothing deploys, so <span class="accent accent-point">the live site stays on the last good version</span>.</li>
<li>The base URL comes from the repo&rsquo;s Pages settings at build time, so the same workflow worked at
<code>erikfailing.github.io</code> first and then at <code>erikfailing.com</code> <span class="accent accent-trophy">with zero changes</span>.</li>
</ul>
<h2 id="pointing-the-domain-at-it">Pointing The Domain At It</h2>
<p>My domain is registered through <span class="accent accent-iron">Squarespace</span>, so this was mostly clicking through their DNS panel
in the right order:</p>
<ol>
<li><strong><span class="accent accent-point">Verify the domain with GitHub first.</span></strong> GitHub gives you a TXT record
(<code>_github-pages-challenge-&lt;username&gt;</code>), and once it&rsquo;s verified, only my account can publish a
Pages site on that domain. This prevents <span class="accent accent-crimson">domain takeover</span> if the repo ever gets deleted or Pages
gets turned off while DNS still points at GitHub.</li>
<li><strong>Delete Squarespace&rsquo;s default records</strong> (their parking page A records and <code>www</code> CNAME).</li>
<li><strong>Add GitHub&rsquo;s records:</strong> four <code>A</code> records and four <code>AAAA</code> records for the apex, plus a <code>CNAME</code>
for <code>www</code> pointing to <code>erikfailing.github.io</code>.</li>
<li><strong>Set the custom domain in the repo and turn on <span class="accent accent-ward">Enforce HTTPS</span></strong> once GitHub finished issuing the
certificate.</li>
</ol>
<p>No <span class="accent accent-crimson">wildcard records</span>, since a wildcard is an easy way to end up with a subdomain someone else can
claim.</p>
<h2 id="a-few-security-flavored-touches">A Few Security-Flavored Touches</h2>
<p>It&rsquo;s a personal site, so I didn&rsquo;t go overboard, but there were a few things I wanted as a
security engineer:</p>
<ul>
<li><strong><code>security.txt</code>.</strong> There&rsquo;s an RFC 9116 file at <code>/.well-known/security.txt</code> that points to the
repo&rsquo;s <span class="accent accent-ward">private vulnerability reporting</span>, plus a short <code>SECURITY.md</code> policy. It has an <code>Expires</code>
field, so it&rsquo;s a once-a-year chore to bump the date.</li>
<li><strong>No third-party requests for fonts.</strong> I self-host the font instead of loading it from Google
Fonts, so loading a page <span class="accent accent-ward">doesn&rsquo;t send visitors&rsquo; IPs anywhere else</span>.</li>
<li><strong><code>robots.txt</code> that blocks <span class="accent accent-arcane">AI training but not AI search</span>.</strong> It turns out the big AI companies
use separate crawler names for training and for search (e.g. <code>GPTBot</code> vs <code>OAI-SearchBot</code>,
<code>ClaudeBot</code> vs <code>Claude-SearchBot</code>). So I asked the training crawlers to stay out while still
letting search engines and AI search assistants read and cite the site:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">User-agent: GPTBot
</span></span><span class="line"><span class="cl">User-agent: ClaudeBot
</span></span><span class="line"><span class="cl">User-agent: Google-Extended
</span></span><span class="line"><span class="cl">User-agent: Applebot-Extended
</span></span><span class="line"><span class="cl">User-agent: Meta-ExternalAgent
</span></span><span class="line"><span class="cl">User-agent: CCBot
</span></span><span class="line"><span class="cl">User-agent: Bytespider
</span></span><span class="line"><span class="cl">Disallow: /
</span></span></code></pre></div><p>(robots.txt is a request, not access control, but the reputable crawlers do honor it.)</p>
<p><strong>Update (October 31, 2026):</strong> I took these rules back out. They only stop crawlers polite enough
to ask, so they weren&rsquo;t keeping out anyone I&rsquo;d actually worry about, and I&rsquo;d rather AI assistants
know who I am when someone asks about me. The site also has an <code>llms.txt</code> now, a plain-text list
of its pages for AI tools.</p>
<h2 id="a-font-flicker-i-had-to-fix">A Font Flicker I Had To Fix</h2>
<p><span class="accent accent-clarity">Open Sans</span> is my favorite font, so the whole site uses it, self-hosted as one variable font file.
The first version had an annoying bug though: clicking between pages, the text would visibly
shift. It turned out to be a <span class="accent accent-static accent-grad">font swap on every page load</span>. The browser only discovered the font
after parsing the CSS, and <code>font-display: swap</code> told it to draw the text in the system font first
(Segoe UI on Windows) and then redraw it in Open Sans. The two fonts have different widths, so
everything jumped. <span class="accent accent-point">The fix was preloading the font</span>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;preload&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/fonts/open-sans-latin-wght-normal.woff2&#34;</span> <span class="na">as</span><span class="o">=</span><span class="s">&#34;font&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;font/woff2&#34;</span> <span class="na">crossorigin</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>plus switching to <code>font-display: block</code>, so the browser waits the few milliseconds for the
preloaded font instead of painting a stand-in.</p>
<p>The colors got a similar amount of care: every text color passes <span class="accent accent-clarity">WCAG AAA contrast</span> against the
background, and links stay underlined since my link blue is too close in brightness to the body
text for color alone to mark them.</p>
<h2 id="how-i-used-ai-on-this">How I Used AI On This</h2>
<p>I used <span class="accent accent-claude">Claude Code</span> (an LLM coding assistant) a lot on this project, and I think it was genuinely
useful, but <span class="accent accent-point">as an assistant, not as the one in charge</span>. It drafted config, looked up docs, ran test
builds, and did research I&rsquo;d otherwise do by hand (like verifying every crawler name against each
company&rsquo;s docs). I still made the decisions and read every change before it went in.</p>
<p>And reading everything mattered. A few examples:</p>
<ul>
<li>On the very first step, the submodule command it wrote had the wrong GitHub username for
PaperMod&rsquo;s author. That would have pulled a theme from a repo that wasn&rsquo;t the real one, which is
a <span class="accent accent-iron">supply-chain mistake in a single typo</span>. It got blocked before running, and the correct URL went
in instead, but it&rsquo;s exactly the kind of thing you only catch by actually reading the command.</li>
<li>The font flicker above was something I noticed clicking around the site myself. The first font
setup it gave me &ldquo;worked,&rdquo; it just wasn&rsquo;t right.</li>
</ul>
<p>So overall, <span class="accent accent-point">I think it saved me a lot of time on the tedious parts</span> (looking up DNS records,
reading theme source to find the right CSS selectors, etc&hellip;), and it left me more time for the
parts I actually care about. But anything it hands me still gets read before it ships.</p>
<h2 id="whats-next">What&rsquo;s Next</h2>
<p>No big roadmap. I&rsquo;ll update the site as I feel like it, mostly with <span class="accent accent-crimson">security projects</span> and
writeups, plus some software dev stuff here and there.</p>
]]></content:encoded></item></channel></rss>