<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>felipeweber.com – felipeweber.com</title><link>/en/</link><description>Recent content on felipeweber.com</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>weberecomp@gmail.com (Felipe Weber)</managingEditor><webMaster>weberecomp@gmail.com (Felipe Weber)</webMaster><lastBuildDate>Wed, 02 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="/en/index.xml" rel="self" type="application/rss+xml"/><item><title>Shipping a Hugo site on Cloudflare Pages without the headache</title><link>/en/2026/09/02/cloudflare-pages-hugo/</link><pubDate>Wed, 02 Sep 2026 00:00:00 +0000</pubDate><author>weberecomp@gmail.com (Felipe Weber)</author><guid>/en/2026/09/02/cloudflare-pages-hugo/</guid><description>
&lt;p>Putting a static site online stopped being a project. What still eats time is
the first half hour — the one between &amp;ldquo;works on my machine&amp;rdquo; and &amp;ldquo;works in
production, with HTTPS and automatic deploys&amp;rdquo;.&lt;/p>
&lt;p>Here is the path I use today.&lt;/p>
&lt;h2>The minimum contract&lt;span class="hx:absolute hx:-mt-20" id="the-minimum-contract">&lt;/span>
&lt;a href="#the-minimum-contract" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Cloudflare Pages needs three things: a Git repository, a build command and an
output directory. For Hugo:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Build command&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">hugo --gc --minify
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Build output directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">public&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>The rest is environment configuration. And that is where almost every first
deploy fails.&lt;/p>
&lt;h2>Trap 1: the Hugo version&lt;span class="hx:absolute hx:-mt-20" id="trap-1-the-hugo-version">&lt;/span>
&lt;a href="#trap-1-the-hugo-version" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>The Pages runner does not use the version you have locally. Without pinning,
you get an old one — usually without the extended build, which breaks any
theme that touches SCSS.&lt;/p>
&lt;p>Set the environment variable in the project settings:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">HUGO_VERSION&lt;/span> &lt;span class="o">=&lt;/span> 0.165.0&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;blockquote>
&lt;p>Pin the version in the repository too, in a file people can actually read.
An invisible environment variable is technical debt waiting to happen.&lt;/p>
&lt;/blockquote>
&lt;h2>Trap 2: Hugo Modules need Go&lt;span class="hx:absolute hx:-mt-20" id="trap-2-hugo-modules-need-go">&lt;/span>
&lt;a href="#trap-2-hugo-modules-need-go" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>If the theme ships as a Hugo Module — and most do now — the runner needs Go
available before &lt;code>hugo&lt;/code> runs:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">GO_VERSION&lt;/span> &lt;span class="o">=&lt;/span> 1.27.1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>Without it the build dies with a &lt;code>module not found&lt;/code> that says nothing about
the actual cause.&lt;/p>
&lt;h2>Trap 3: &lt;code>baseURL&lt;/code> and previews&lt;span class="hx:absolute hx:-mt-20" id="trap-3-baseurl-and-previews">&lt;/span>
&lt;a href="#trap-3-baseurl-and-previews" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Every branch gets its own preview URL. If &lt;code>baseURL&lt;/code> is hardcoded in the
config, the preview&amp;rsquo;s absolute links point at production. The fix is to let
Pages inject the value:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hugo --gc --minify --baseURL &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CF_PAGES_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>In production &lt;code>CF_PAGES_URL&lt;/code> is the final domain, so one command covers both
cases with no branching.&lt;/p>
&lt;h2>What you get for free&lt;span class="hx:absolute hx:-mt-20" id="what-you-get-for-free">&lt;/span>
&lt;a href="#what-you-get-for-free" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>After that, every &lt;code>git push&lt;/code> to &lt;code>main&lt;/code> is a deploy. Branches become previews.
The certificate is issued and renewed on its own. And the cost stays at zero
up to traffic most personal blogs will never see.&lt;/p>
&lt;p>The official docs live at
&lt;a href="https://developers.cloudflare.com/pages/"target="_blank" rel="noopener">developers.cloudflare.com/pages&lt;/a>,
and the build configuration section is worth reading before the first deploy.&lt;/p></description></item><item><title>Hugo Modules in practice: versioned themes without submodules</title><link>/en/2026/08/18/hugo-modules-in-practice/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><author>weberecomp@gmail.com (Felipe Weber)</author><guid>/en/2026/08/18/hugo-modules-in-practice/</guid><description>
&lt;p>Every Hugo tutorial starts with &lt;code>git submodule add&lt;/code>. It works, but it turns
the theme into a frozen copy inside your repository, with no declared version
and no upgrade path.&lt;/p>
&lt;p>Hugo Modules fix that by treating the theme as what it is: a dependency.&lt;/p>
&lt;h2>The setup&lt;span class="hx:absolute hx:-mt-20" id="the-setup">&lt;/span>
&lt;a href="#the-setup" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hugo mod init github.com/your-user/your-site
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">hugo mod get github.com/imfing/hextra&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>The generated &lt;code>go.mod&lt;/code> records the exact version:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;pre>&lt;code>module github.com/your-user/your-site
go 1.27.1
require github.com/imfing/hextra v0.12.3&lt;/code>&lt;/pre>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>In the config, the import replaces the old &lt;code>theme:&lt;/code> key:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">module&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github.com/imfing/hextra&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;h2>Why this is better&lt;span class="hx:absolute hx:-mt-20" id="why-this-is-better">&lt;/span>
&lt;a href="#why-this-is-better" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Upgrading is one command, and so is rolling back:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hugo mod get -u github.com/imfing/hextra &lt;span class="c1"># latest&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">hugo mod get github.com/imfing/hextra@v0.12.3 &lt;span class="c1"># back to a specific one&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>No theme file ever enters your repository. The diff of an upgrade is one line
in &lt;code>go.mod&lt;/code>.&lt;/p>
&lt;h2>File-by-file overrides&lt;span class="hx:absolute hx:-mt-20" id="file-by-file-overrides">&lt;/span>
&lt;a href="#file-by-file-overrides" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>The part that sold me: project files beat module files, one at a time. If you
want to change only the footer, you create only the footer:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;pre>&lt;code>layouts/_partials/footer.html&lt;/code>&lt;/pre>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>The rest of the theme keeps coming from the module and keeps receiving fixes
on upgrade. Compared to vendoring the whole theme, the maintenance difference
is large.&lt;/p>
&lt;blockquote>
&lt;p>Rule of thumb: if you have copied more than three theme files, check whether
the problem is actually configuration.&lt;/p>
&lt;/blockquote>
&lt;h2>The cost&lt;span class="hx:absolute hx:-mt-20" id="the-cost">&lt;/span>
&lt;a href="#the-cost" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>You need Go installed — locally and in CI. That is the entry fee, and it is
worth paying, because from then on the theme stops being an opaque blob in
your commit history.&lt;/p>
&lt;p>Hextra&amp;rsquo;s source lives at
&lt;a href="https://github.com/imfing/hextra"target="_blank" rel="noopener">github.com/imfing/hextra&lt;/a> and is a good
reference for how a modular theme is organized.&lt;/p></description></item><item><title>A static search index that fits in 40 KB</title><link>/en/2026/08/04/static-search-index/</link><pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate><author>weberecomp@gmail.com (Felipe Weber)</author><guid>/en/2026/08/04/static-search-index/</guid><description>
&lt;p>Search on a static site has three paths: a paid external service, your own
server, or a JSON index built at compile time and queried in the browser. For
a blog the third one is almost always right — as long as the index stays thin.&lt;/p>
&lt;h2>What makes an index fat&lt;span class="hx:absolute hx:-mt-20" id="what-makes-an-index-fat">&lt;/span>
&lt;a href="#what-makes-an-index-fat" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>The common mistake is indexing the full body of every post. Fifty
1,500-word articles turn into a multi-megabyte JSON file the visitor
downloads before typing a single letter.&lt;/p>
&lt;p>What actually matters for blog search:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Good for&lt;/th>
&lt;th>Cost&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>title&lt;/td>
&lt;td>finding the post&lt;/td>
&lt;td>tiny&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>description&lt;/td>
&lt;td>context in results&lt;/td>
&lt;td>low&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>headings&lt;/td>
&lt;td>finding the section&lt;/td>
&lt;td>low&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>full body&lt;/td>
&lt;td>matching an exact phrase&lt;/td>
&lt;td>enormous&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In practice title + description + headings answer nearly every real query and
keep the index in the tens of kilobytes.&lt;/p>
&lt;h2>Generating it at build time&lt;span class="hx:absolute hx:-mt-20" id="generating-it-at-build-time">&lt;/span>
&lt;a href="#generating-it-at-build-time" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>In Hugo the index is just another output template. The relevant part:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$index&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">slice&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">range&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">where&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">site&lt;/span>&lt;span class="na">.RegularPages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Section&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;blog&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$index&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$index&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nx">dict&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Title&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.RelPermalink&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;desc&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Description&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;headings&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nx">apply&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Fragments.Headings&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;.&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Title&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$index&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>No extra pipeline step: the file is born alongside the HTML.&lt;/p>
&lt;h2>Load it late&lt;span class="hx:absolute hx:-mt-20" id="load-it-late">&lt;/span>
&lt;a href="#load-it-late" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>The index does not need to exist at first paint. Fetch it when the field is
first focused:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span> &lt;span class="nx">indexPromise&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;focus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">indexPromise&lt;/span> &lt;span class="o">??=&lt;/span> &lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/search-index.json&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">once&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>Visitors who never search never pay for it. Those who do pay once, and the
browser caches it.&lt;/p>
&lt;blockquote>
&lt;p>The best optimization is still not downloading the file.&lt;/p>
&lt;/blockquote>
&lt;h2>Where this breaks&lt;span class="hx:absolute hx:-mt-20" id="where-this-breaks">&lt;/span>
&lt;a href="#where-this-breaks" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>If your use case is matching an exact phrase inside a long document — a
manual, a knowledge base — the reduced index will not do. There it is worth
evaluating &lt;a href="https://pagefind.app/"target="_blank" rel="noopener">Pagefind&lt;/a>, which shards the index and only
downloads the pieces it needs.&lt;/p>
&lt;p>For a blog, that is optimizing a problem you do not have.&lt;/p></description></item><item><title>Dark mode with no flash: what happens before first paint</title><link>/en/2026/07/21/dark-mode-no-flash/</link><pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate><author>weberecomp@gmail.com (Felipe Weber)</author><guid>/en/2026/07/21/dark-mode-no-flash/</guid><description>
&lt;p>You implement dark mode, you test it, it works. Then you reload and see a
white flash before the dark theme appears. The classic FOUC.&lt;/p>
&lt;p>The cause is not in the CSS. It is in &lt;em>when&lt;/em> the JavaScript runs.&lt;/p>
&lt;h2>The race&lt;span class="hx:absolute hx:-mt-20" id="the-race">&lt;/span>
&lt;a href="#the-race" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>The browser builds the page in order: parse HTML, fetch CSS, compute style,
paint. If the script that decides the theme runs &lt;em>after&lt;/em> that paint, the user
sees the default theme first.&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;pre>&lt;code>parse HTML → fetch CSS → first paint → DOMContentLoaded → your script
▲ │
flash happens here theme applied here&lt;/code>&lt;/pre>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>Any script with &lt;code>defer&lt;/code>, &lt;code>async&lt;/code>, or bound to &lt;code>DOMContentLoaded&lt;/code> loses the
race by construction.&lt;/p>
&lt;h2>The fix&lt;span class="hx:absolute hx:-mt-20" id="the-fix">&lt;/span>
&lt;a href="#the-fix" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>A &lt;strong>synchronous inline&lt;/strong> script in &lt;code>&amp;lt;head&amp;gt;&lt;/code>, before any stylesheet that
depends on the theme:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">stored&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">localStorage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getItem&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;color-theme&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">theme&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">stored&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s2">&amp;#34;light&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">stored&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s2">&amp;#34;dark&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?&lt;/span> &lt;span class="nx">stored&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;(prefers-color-scheme: dark)&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">matches&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="s2">&amp;#34;dark&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;light&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">theme&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">colorScheme&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">theme&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>It blocks the parser for well under a millisecond, and the class is already on
&lt;code>&amp;lt;html&amp;gt;&lt;/code> when the first pixel appears.&lt;/p>
&lt;h2>The two details almost everyone forgets&lt;span class="hx:absolute hx:-mt-20" id="the-two-details-almost-everyone-forgets">&lt;/span>
&lt;a href="#the-two-details-almost-everyone-forgets" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>&lt;strong>&lt;code>color-scheme&lt;/code>.&lt;/strong> Without it the browser still paints scrollbars, form
controls and the default canvas in light. It is one line, and it is the
difference between &amp;ldquo;dark&amp;rdquo; and &lt;em>actually&lt;/em> dark.&lt;/p>
&lt;p>&lt;strong>&lt;code>localStorage&lt;/code> can throw.&lt;/strong> In some browsers&amp;rsquo; private mode, accessing it
raises. Wrap it in &lt;code>try/catch&lt;/code> — the site has to load even when the preference
cannot be read.&lt;/p>
&lt;blockquote>
&lt;p>Three states, not two: light, dark and &lt;em>system&lt;/em>. System is the correct
default, and the only one that respects someone who changed their OS
preference after visiting your site.&lt;/p>
&lt;/blockquote>
&lt;h2>Verifying it&lt;span class="hx:absolute hx:-mt-20" id="verifying-it">&lt;/span>
&lt;a href="#verifying-it" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>The honest test is with CPU throttling on, 6x slowdown, cache disabled. The
flash you cannot see on your desktop shows up immediately on a mid-range
phone.&lt;/p></description></item><item><title>Writing CSS that survives the next redesign</title><link>/en/2026/06/30/css-that-survives/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><author>weberecomp@gmail.com (Felipe Weber)</author><guid>/en/2026/06/30/css-that-survives/</guid><description>
&lt;p>Every frontend project reaches the point where nobody wants to touch the CSS.
Not because it is badly written — because nobody knows what will break.&lt;/p>
&lt;p>The symptom is always the same: the same decision is written in fifteen
places.&lt;/p>
&lt;h2>Tokens before components&lt;span class="hx:absolute hx:-mt-20" id="tokens-before-components">&lt;/span>
&lt;a href="#tokens-before-components" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Before styling anything, define the vocabulary:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">root&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--surface&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#faf7f0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--text&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#23201c&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--text-muted&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">rgb&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">35&lt;/span> &lt;span class="mi">32&lt;/span> &lt;span class="mi">28&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">62&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--accent&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#9a5b18&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--radius&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--measure&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">ch&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>Swapping the whole theme becomes swapping that block. Without tokens, it is a
find-and-replace with risk attached.&lt;/p>
&lt;h2>One theme, one inversion&lt;span class="hx:absolute hx:-mt-20" id="one-theme-one-inversion">&lt;/span>
&lt;a href="#one-theme-one-inversion" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Dark mode is not a second stylesheet. It is the same sheet with the tokens
redefined:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">html&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">dark&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--surface&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#1a1714&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--text&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#ece7de&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--accent&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#e2a05a&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>If you had to write &lt;code>html.dark .component { ... }&lt;/code>, the component is reading a
color that should have been a token.&lt;/p>
&lt;h2>&lt;code>color-mix&lt;/code> kills the twelve-shade palette&lt;span class="hx:absolute hx:-mt-20" id="color-mix-kills-the-twelve-shade-palette">&lt;/span>
&lt;a href="#color-mix-kills-the-twelve-shade-palette" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Half the variables in a design system are opacity variants of the same color.
&lt;code>color-mix&lt;/code> derives them on the spot:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">card&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="kc">solid&lt;/span> &lt;span class="nf">color-mix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">in&lt;/span> &lt;span class="n">srgb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="kc">text&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">transparent&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">card&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">hover&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">border-color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">color-mix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">in&lt;/span> &lt;span class="n">srgb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">accent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">48&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">transparent&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>The border follows the theme on its own, in both modes, with no new variable.&lt;/p>
&lt;h2>Measure is a decision, not an accident&lt;span class="hx:absolute hx:-mt-20" id="measure-is-a-decision-not-an-accident">&lt;/span>
&lt;a href="#measure-is-a-decision-not-an-accident" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">prose&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">max-width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">ch&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">line-height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.7&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>Two lines that do more for readability than any font choice. &lt;code>ch&lt;/code> tracks the
text size — if the font grows, the measure grows with it.&lt;/p>
&lt;blockquote>
&lt;p>If you cannot explain why a value is &lt;code>1.7&lt;/code> and not &lt;code>1.5&lt;/code>, it will become
&lt;code>1.6&lt;/code> in the next PR and nobody will be able to say whether that helped.&lt;/p>
&lt;/blockquote>
&lt;h2>The test&lt;span class="hx:absolute hx:-mt-20" id="the-test">&lt;/span>
&lt;a href="#the-test" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Take the stylesheet and try to change the site&amp;rsquo;s accent color. If it is one
line, it survives the next redesign. If it takes five files, you already know
where refactoring starts.&lt;/p></description></item><item><title>Measuring Core Web Vitals for real on a static site</title><link>/en/2026/05/14/real-core-web-vitals/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><author>weberecomp@gmail.com (Felipe Weber)</author><guid>/en/2026/05/14/real-core-web-vitals/</guid><description>
&lt;p>Static sites have an unfair advantage in performance: no server thinking, no
hydration, no API waterfall. Scoring 100 in Lighthouse is easy.&lt;/p>
&lt;p>Which is exactly why the number misleads.&lt;/p>
&lt;h2>Lab is not field&lt;span class="hx:absolute hx:-mt-20" id="lab-is-not-field">&lt;/span>
&lt;a href="#lab-is-not-field" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>Lighthouse runs a simulation: one machine, one network, one cold load. The
Core Web Vitals Google actually uses come from CrUX — real users, real
devices, real networks, at the 75th percentile.&lt;/p>
&lt;p>The two disagree often, and the field wins.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Measurement&lt;/th>
&lt;th>Where it lives&lt;/th>
&lt;th>What it answers&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Lighthouse&lt;/td>
&lt;td>your terminal / CI&lt;/td>
&lt;td>&amp;ldquo;did I regress since last commit?&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CrUX&lt;/td>
&lt;td>aggregated real data&lt;/td>
&lt;td>&amp;ldquo;are my visitors suffering?&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Your own RUM&lt;/td>
&lt;td>your site&lt;/td>
&lt;td>&amp;ldquo;which pages, which devices?&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2>The three metrics, one sentence each&lt;span class="hx:absolute hx:-mt-20" id="the-three-metrics-one-sentence-each">&lt;/span>
&lt;a href="#the-three-metrics-one-sentence-each" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>&lt;strong>LCP&lt;/strong> — when the largest visible element finished painting. On a blog it is
almost always the title or the first image. A badly configured web font delays
it more than any image will.&lt;/p>
&lt;p>&lt;strong>INP&lt;/strong> — how long the page takes to respond to an interaction. Static sites
rarely fail here, unless a third-party script is holding the main thread.&lt;/p>
&lt;p>&lt;strong>CLS&lt;/strong> — how much content jumps during load. Cause number one: images without
&lt;code>width&lt;/code>/&lt;code>height&lt;/code>. Cause number two: a font swap that changes text height.&lt;/p>
&lt;h2>Measuring on your own site&lt;span class="hx:absolute hx:-mt-20" id="measuring-on-your-own-site">&lt;/span>
&lt;a href="#measuring-on-your-own-site" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;p>The official library fits in a few lines and reports real numbers:&lt;/p>
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
&lt;div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">onLCP&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">onINP&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">onCLS&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;web-vitals&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">metric&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">navigator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sendBeacon&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/vitals&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">stringify&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">metric&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">metric&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rating&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">metric&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">rating&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">path&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">location&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">pathname&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">onLCP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">onINP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">onCLS&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4">&lt;/div>
&lt;/button>
&lt;/div>
&lt;/div>
&lt;p>&lt;code>sendBeacon&lt;/code> does not hold navigation — the payload goes out even after the
user has left the page.&lt;/p>
&lt;h2>What to fix first on a static site&lt;span class="hx:absolute hx:-mt-20" id="what-to-fix-first-on-a-static-site">&lt;/span>
&lt;a href="#what-to-fix-first-on-a-static-site" class="subheading-anchor" aria-label="Permalink for this section">&lt;/a>&lt;/h2>&lt;ol>
&lt;li>&lt;strong>Fonts.&lt;/strong> &lt;code>font-display: swap&lt;/code>, &lt;code>preload&lt;/code> only the files used above the
fold, and serve them from your own domain.&lt;/li>
&lt;li>&lt;strong>Image dimensions.&lt;/strong> Always &lt;code>width&lt;/code> and &lt;code>height&lt;/code>, even with responsive CSS
on top.&lt;/li>
&lt;li>&lt;strong>Third-party scripts.&lt;/strong> Each one is an INP risk you do not control.&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>Optimize from the inside out: first what you serve, then what you borrow.&lt;/p>
&lt;/blockquote>
&lt;p>The reference that keeps up with metric definition changes is
&lt;a href="https://web.dev/vitals/"target="_blank" rel="noopener">web.dev/vitals&lt;/a>.&lt;/p></description></item></channel></rss>