<?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>Posts on Canh Dinh</title><link>https://blog.canhdinh.com/posts/</link><description>Recent content in Posts on Canh Dinh</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 28 Jun 2026 16:10:00 +0700</lastBuildDate><atom:link href="https://blog.canhdinh.com/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Seamless Secret Management in GitOps With SOPS and age</title><link>https://blog.canhdinh.com/posts/seamless-secret-management-with-sops-and-age/</link><pubDate>Sun, 28 Jun 2026 16:10:00 +0700</pubDate><guid>https://blog.canhdinh.com/posts/seamless-secret-management-with-sops-and-age/</guid><description>&lt;p&gt;GitOps has a famously awkward edge case: you want &lt;em&gt;everything&lt;/em&gt; in Git, but you can&amp;rsquo;t commit a plaintext database password to a repository.&lt;/p&gt;
&lt;p&gt;The usual workarounds — a separate secrets manager, a wall of &lt;code&gt;kubectl create secret&lt;/code&gt; commands, a shared password vault that nobody keeps in sync — all break the &amp;ldquo;Git is the source of truth&amp;rdquo; promise.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/getsops/sops"&gt;SOPS&lt;/a&gt; (Secrets OPerationS) and &lt;a href="https://github.com/FiloSottile/age"&gt;age&lt;/a&gt; solve this neatly. Together they let you commit &lt;strong&gt;encrypted&lt;/strong&gt; secrets straight into Git, review them in pull requests, and decrypt them only where they&amp;rsquo;re needed.&lt;/p&gt;</description><content:encoded><![CDATA[<p>GitOps has a famously awkward edge case: you want <em>everything</em> in Git, but you can&rsquo;t commit a plaintext database password to a repository.</p>
<p>The usual workarounds — a separate secrets manager, a wall of <code>kubectl create secret</code> commands, a shared password vault that nobody keeps in sync — all break the &ldquo;Git is the source of truth&rdquo; promise.</p>
<p><a href="https://github.com/getsops/sops">SOPS</a> (Secrets OPerationS) and <a href="https://github.com/FiloSottile/age">age</a> solve this neatly. Together they let you commit <strong>encrypted</strong> secrets straight into Git, review them in pull requests, and decrypt them only where they&rsquo;re needed.</p>
<p>The plaintext never touches the repo; the ciphertext lives right next to the code it belongs to.</p>
<p>This post covers what each tool is, how the encryption works under the hood (including the role of public keys), and why the combination fits GitOps so well.</p>
<p>It then shows how the pair enables team collaboration, walks through a runnable example that uses <a href="https://mise.jdx.dev/"><code>mise</code></a> to install both tools, and lists the limitations to know before adopting it.</p>
<h2 id="what-are-sops-and-age">What are SOPS and age?</h2>
<p>They solve two different halves of the same problem.</p>
<p><strong>age</strong> is a modern, opinionated file-encryption tool — think &ldquo;GPG without the footguns&rdquo;. It has no configuration knobs, no cipher negotiation, and tiny keys.</p>
<p>A public key looks like <code>age1nwhnh2qv6yealq4npum4tlzl0uyev5haa7y355znqjhwuxu8l3qsc4h8mc</code> and a private key is a single line you can paste anywhere. age encrypts a whole file (or stdin) for one or more recipients. That&rsquo;s it.</p>
<p><strong>SOPS</strong> is an <em>editor</em> for structured secret files — YAML, JSON, ENV, INI, or binary. Instead of encrypting the whole file into an opaque blob, it encrypts only the <strong>values</strong>, leaving keys, structure, and comments readable.</p>
<p>SOPS delegates the actual cryptography to a backend: AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault, PGP — or <strong>age</strong>.</p>
<p>The combination is powerful precisely because each tool stays in its lane. age provides simple, auditable encryption with portable keys; SOPS provides a structure-aware, Git-friendly workflow on top of it.</p>
<p>No cloud account required.</p>
<h2 id="how-it-works-internally">How it works internally</h2>
<p>This is the part worth understanding, because it explains every design decision that follows.</p>
<h3 id="age-envelope-encryption-with-x25519">age: envelope encryption with X25519</h3>
<p>age uses a classic <strong>envelope (hybrid) encryption</strong> scheme. When you encrypt a file for a recipient&rsquo;s public key:</p>
<ol>
<li>age generates a random 128-bit (16-byte) symmetric <strong>file key</strong> for this one file.</li>
<li>The file body is encrypted with a <strong>payload key</strong> derived from the file key (via <code>HKDF-SHA-256</code>, salted with a random nonce) using <strong>ChaCha20-Poly1305</strong>, an authenticated cipher (confidentiality <em>and</em> integrity). The body is split into 64 KiB chunks so it can be streamed.</li>
<li>For each recipient, age <strong>wraps</strong> (encrypts) the file key so only that recipient&rsquo;s private key can unwrap it. This wrapped copy is stored in a per-recipient <em>stanza</em> in the file header.</li>
</ol>
<p>The wrapping for an <code>age1...</code> recipient uses <strong>X25519</strong>, an Elliptic-Curve Diffie–Hellman function over Curve25519:</p>
<ul>
<li>age generates an <strong>ephemeral keypair</strong> just for this encryption.</li>
<li>It combines the ephemeral private key with the recipient&rsquo;s public key via Diffie–Hellman to derive a shared secret.</li>
<li>It runs that shared secret through <code>HKDF-SHA-256</code> to get a <em>wrap key</em>, which encrypts the file key into the recipient&rsquo;s stanza. The stanza also stores the ephemeral <em>public</em> key.</li>
</ul>
<p>To decrypt, the recipient combines their <strong>private</strong> key with the stored ephemeral public key, derives the <em>same</em> shared secret, unwraps the file key, and decrypts the body.</p>
<p>The math of Diffie–Hellman guarantees both sides arrive at the same secret without it ever crossing the wire.</p>
<p>The key insight: <strong>the public key only lets you wrap (encrypt) the file key; it cannot unwrap it.</strong> So a public key is safe to share, commit, and put in a PR.</p>
<p>You can encrypt <em>for</em> someone without being able to decrypt what you just wrote — and that&rsquo;s exactly what makes multi-recipient, GitOps-friendly secrets possible.</p>
<p>Because the file key is wrapped once per recipient, encrypting for ten teammates just means ten small stanzas in front of one shared ciphertext body.</p>
<p>An age file header is plainly visible:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">-----BEGIN AGE ENCRYPTED FILE-----
</span></span><span class="line"><span class="cl">-&gt; X25519 kR7t8qV5zwYtVRhb...       # ephemeral pubkey + wrapped file key (recipient 1)
</span></span><span class="line"><span class="cl">-&gt; X25519 9aB2cD...                 # recipient 2
</span></span><span class="line"><span class="cl">--- &lt;header MAC, HMAC-SHA-256 over the header&gt;
</span></span><span class="line"><span class="cl">&lt;ciphertext body, ChaCha20-Poly1305 in 64 KiB chunks&gt;
</span></span><span class="line"><span class="cl">-----END AGE ENCRYPTED FILE-----
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="sops-a-data-key-on-top-of-age">SOPS: a data key on top of age</h3>
<p>SOPS adds one more layer so it can encrypt values <em>individually</em> while still only doing public-key crypto once:</p>
<ol>
<li>SOPS generates a random <strong>data key</strong> (an AES-256 key) for the file.</li>
<li>It encrypts <strong>each secret value</strong> in place with that data key using <strong>AES256-GCM</strong> — so <code>password: hunter2</code> becomes <code>password: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]</code>. Keys and structure stay in cleartext, which is what makes diffs reviewable.</li>
<li>The data key itself is then encrypted <strong>for every configured recipient</strong>. With the age backend, that means handing the data key to age, which wraps it for each <code>age1...</code> public key and stores the resulting age blob in the file&rsquo;s <code>sops:</code> metadata block.</li>
<li>SOPS computes a <strong>MAC</strong> — a <code>SHA-512</code> hash over all the plaintext values — and stores it <em>encrypted with the data key</em> (AES256-GCM), which is the <code>mac: ENC[AES256_GCM,...]</code> field you&rsquo;ll see in the file. On decryption SOPS recomputes the hash over the decrypted values and compares; if they differ (e.g. someone added, removed, or swapped a ciphertext value), decryption fails.</li>
</ol>
<p>So there are two nested envelopes: <strong>age wraps the SOPS data key</strong> (per recipient, via X25519), and <strong>the data key encrypts each value</strong> (via AES-GCM).</p>
<p>To decrypt, SOPS asks age to unwrap the data key with your private key, then decrypts every <code>ENC[...]</code> value and verifies the MAC.</p>
<p>You&rsquo;ll see this structure directly in an encrypted file&rsquo;s footer:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">sops</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">age</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">enc</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            -----BEGIN AGE ENCRYPTED FILE-----   # the data key, wrapped by age
</span></span></span><span class="line"><span class="cl"><span class="sd">            ...
</span></span></span><span class="line"><span class="cl"><span class="sd">            -----END AGE ENCRYPTED FILE-----</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">recipient</span><span class="p">:</span><span class="w"> </span><span class="l">age1nwhnh2qv6yealq4npum4tlzl0uyev5haa7y355znqjhwuxu8l3qsc4h8mc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">encrypted_regex</span><span class="p">:</span><span class="w"> </span><span class="l">^(data|stringData)$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mac</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:...]                </span><span class="w"> </span><span class="c"># integrity check</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">3.13.1</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="why-this-combination-and-common-use-cases">Why this combination, and common use cases</h2>
<p>A few properties fall out of the design above:</p>
<ul>
<li><strong>Secrets live in Git.</strong> The ciphertext is committed alongside the manifests it configures. One repo, one source of truth — the GitOps ideal.</li>
<li><strong>Diffs stay meaningful.</strong> Because only values are encrypted, a PR shows <em>which</em> keys changed, even if it can&rsquo;t show the new plaintext. Reviewers see structure and intent.</li>
<li><strong>No central secret server required.</strong> Decryption needs only a private key file. Great for homelabs, edge, air-gapped, or &ldquo;I don&rsquo;t want to pay for a KMS&rdquo; setups. (You <em>can</em> still use KMS — SOPS supports mixing backends.)</li>
<li><strong>Asymmetric trust.</strong> Anyone with the public key can add or update secrets; only holders of a private key can read them. CI can encrypt without being able to decrypt.</li>
</ul>
<p>Typical use cases:</p>
<ul>
<li><strong>Kubernetes secrets in GitOps.</strong> Commit encrypted <code>Secret</code> manifests and let <a href="https://fluxcd.io/flux/guides/mozilla-sops/">Flux&rsquo;s SOPS integration</a> or <a href="https://github.com/viaduct-ai/kustomize-sops"><code>ksops</code></a> <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/">for Argo CD</a> decrypt them in-cluster using a private key stored once as a cluster secret.</li>
<li><strong>App config / <code>.env</code> files.</strong> Encrypt <code>config.prod.yaml</code> or <code>.env.production</code> and decrypt at deploy time.</li>
<li><strong>Terraform / Ansible variables.</strong> Keep <code>secrets.auto.tfvars</code> or Ansible vault-style data encrypted in the repo.</li>
<li><strong>CI/CD pipelines.</strong> Store the age private key as a single CI secret; pipelines decrypt everything else from the repo on demand.</li>
</ul>
<h2 id="how-it-enables-team-collaboration">How it enables team collaboration</h2>
<p>This is age&rsquo;s quiet superpower. Because you encrypt for a <em>list</em> of public keys, onboarding a teammate is a metadata change, not a secret re-share.</p>
<ol>
<li>Each engineer (and each environment, and CI) generates their own age keypair and <strong>publishes only the public key</strong> — in the repo, a wiki, or chat. Private keys never leave their owner&rsquo;s machine.</li>
<li>A <code>.sops.yaml</code> file in the repo lists which public keys may decrypt which paths.</li>
<li>To add a new member, you append their public key to <code>.sops.yaml</code> and run <code>sops updatekeys</code> on the affected files. SOPS unwraps the data key, re-wraps it for the new recipient list, and writes the file back — <strong>without ever exposing the plaintext values</strong>. The change is a reviewable diff in the <code>sops:</code> block.</li>
<li>To off-board someone, remove their key and run <code>updatekeys</code> again, then rotate the underlying secrets.</li>
</ol>
<p>You can even encrypt to <em>different</em> recipient sets per path — say, dev secrets readable by the whole team but prod secrets restricted to the CI key and two leads.</p>
<p>All of it is declared in one <code>.sops.yaml</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">creation_rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">path_regex</span><span class="p">:</span><span class="w"> </span><span class="l">secrets/dev/.*\.ya?ml$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">age</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;age1dev...,age1alice...,age1bob...&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">path_regex</span><span class="p">:</span><span class="w"> </span><span class="l">secrets/prod/.*\.ya?ml$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">age</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;age1ci...,age1lead...&#34;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>No shared master password, no &ldquo;DM me the prod creds&rdquo;, no secret that&rsquo;s only in one person&rsquo;s head.</p>
<h2 id="a-runnable-example">A runnable example</h2>
<p>Let&rsquo;s encrypt a Kubernetes <code>Secret</code> end to end. We&rsquo;ll use <a href="https://mise.jdx.dev/"><code>mise</code></a> to install <code>sops</code> and <code>age</code> so the versions are pinned and reproducible. (New to <code>mise</code>? See my <a href="../getting-started-with-mise/">getting started post</a>.)</p>
<h3 id="1-create-the-project-and-install-the-tools">1. Create the project and install the tools</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir sops-age-demo <span class="o">&amp;&amp;</span> <span class="nb">cd</span> sops-age-demo
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># install both tools, pinned in mise.toml</span>
</span></span><span class="line"><span class="cl">mise use sops@latest age@latest
</span></span></code></pre></td></tr></table>
</div>
</div><p>This writes a <code>mise.toml</code> and installs the binaries:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">age</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">sops</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Confirm they&rsquo;re active:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- sops --version    <span class="c1"># sops 3.13.1 (latest)</span>
</span></span><span class="line"><span class="cl">mise <span class="nb">exec</span> -- age --version     <span class="c1"># v1.3.1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><blockquote>
<p>Pin real versions (e.g. <code>sops@3.13.1</code>, <code>age@1.3.1</code>) and commit a <code>mise.lock</code> if you want byte-for-byte reproducibility — see the <a href="https://mise.jdx.dev/configuration/settings.html#lockfile">mise lockfile docs</a>.</p>
</blockquote>
<h3 id="2-generate-an-age-keypair">2. Generate an age keypair</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- age-keygen -o keys.txt
</span></span><span class="line"><span class="cl"><span class="c1"># Public key: age1nwhnh2qv6yealq4npum4tlzl0uyev5haa7y355znqjhwuxu8l3qsc4h8mc</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>keys.txt</code> holds your <strong>private</strong> key — never commit it. The public key is printed and also stored as a comment inside the file. You can re-derive the public key from the private file at any time:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- age-keygen -y keys.txt
</span></span><span class="line"><span class="cl"><span class="c1"># age1nwhnh2qv6yealq4npum4tlzl0uyev5haa7y355znqjhwuxu8l3qsc4h8mc</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Tell SOPS where your private key lives (SOPS reads this env var when decrypting):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">SOPS_AGE_KEY_FILE</span><span class="o">=</span><span class="nv">$PWD</span>/keys.txt
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="3-declare-encryption-rules-in-sopsyaml">3. Declare encryption rules in <code>.sops.yaml</code></h3>
<p>Put your <strong>public</strong> key here. The <code>encrypted_regex</code> tells SOPS to only encrypt values under <code>data</code>/<code>stringData</code>, leaving the rest of the manifest readable:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># .sops.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">creation_rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">path_regex</span><span class="p">:</span><span class="w"> </span><span class="l">secrets/.*\.ya?ml$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">encrypted_regex</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;^(data|stringData)$&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">age</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      age1nwhnh2qv6yealq4npum4tlzl0uyev5haa7y355znqjhwuxu8l3qsc4h8mc</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="4-write-and-encrypt-a-secret">4. Write and encrypt a secret</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p secrets
</span></span><span class="line"><span class="cl">cat &gt; secrets/db.yaml <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: Secret
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: db-credentials
</span></span></span><span class="line"><span class="cl"><span class="s">stringData:
</span></span></span><span class="line"><span class="cl"><span class="s">  username: app
</span></span></span><span class="line"><span class="cl"><span class="s">  password: s3cr3t-p@ssw0rd
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">mise <span class="nb">exec</span> -- sops encrypt --in-place secrets/db.yaml
</span></span></code></pre></td></tr></table>
</div>
</div><p>The result is safe to commit. Note that <code>metadata</code> and the keys stay readable — only the values are <code>ENC[...]</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</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">db-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">stringData</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:/UNE,iv:JpHY...,tag:pEdX...,type:str]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:lk6v7...,iv:NflK...,tag:d9sz...,type:str]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">sops</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">age</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">enc</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            -----BEGIN AGE ENCRYPTED FILE-----
</span></span></span><span class="line"><span class="cl"><span class="sd">            ...the data key, wrapped for your public key...
</span></span></span><span class="line"><span class="cl"><span class="sd">            -----END AGE ENCRYPTED FILE-----</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">recipient</span><span class="p">:</span><span class="w"> </span><span class="l">age1nwhnh2qv6yealq4npum4tlzl0uyev5haa7y355znqjhwuxu8l3qsc4h8mc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">encrypted_regex</span><span class="p">:</span><span class="w"> </span><span class="l">^(data|stringData)$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mac</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:...]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">3.13.1</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="5-decrypt-edit-and-extract">5. Decrypt, edit, and extract</h3>
<p>Decrypt the whole file (needs your private key via <code>SOPS_AGE_KEY_FILE</code>):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- sops decrypt secrets/db.yaml
</span></span><span class="line"><span class="cl"><span class="c1"># ...stringData.password: s3cr3t-p@ssw0rd</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Edit in place — SOPS decrypts into a temp editor session and re-encrypts on save:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- sops edit secrets/db.yaml
</span></span></code></pre></td></tr></table>
</div>
</div><p>Pull out a single value (handy in deploy scripts):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- sops decrypt --extract <span class="s1">&#39;[&#34;stringData&#34;][&#34;password&#34;]&#39;</span> secrets/db.yaml
</span></span><span class="line"><span class="cl"><span class="c1"># s3cr3t-p@ssw0rd</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Pipe a decrypted manifest straight to your cluster, so plaintext never lands on disk:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- sops decrypt secrets/db.yaml <span class="p">|</span> kubectl apply -f -
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="6-add-a-teammate-key-rotation">6. Add a teammate (key rotation)</h3>
<p>Append a second public key to the <code>age:</code> line in <code>.sops.yaml</code>, then re-wrap the data key for the new recipient list — no plaintext exposure:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> -- sops updatekeys secrets/db.yaml
</span></span></code></pre></td></tr></table>
</div>
</div><p>The only change in the diff is a new stanza in the <code>sops:</code> block. The teammate can now decrypt with <em>their</em> private key, and the original <code>ENC[...]</code> values are untouched.</p>
<h3 id="7-what-to-commit-and-what-not-to">7. What to commit (and what not to)</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;keys.txt&#39;</span> &gt;&gt; .gitignore   <span class="c1"># NEVER commit private keys</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git add mise.toml .sops.yaml secrets/db.yaml .gitignore
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Add encrypted db credentials&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Commit the encrypted secret, the <code>.sops.yaml</code>, and <code>mise.toml</code>. Keep <code>keys.txt</code> out of Git — distribute private keys out-of-band (a password manager, your CI&rsquo;s secret store, or a sealed cluster secret).</p>
<h3 id="8-clean-up-optional">8. Clean up (optional)</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> .. <span class="o">&amp;&amp;</span> rm -rf sops-age-demo
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="limitations-and-gotchas">Limitations and gotchas</h2>
<p>No tool is free of trade-offs. Know these before you commit:</p>
<ul>
<li><strong>Key distribution is on you.</strong> age has no PKI, no web of trust, no revocation lists. Getting private keys safely onto machines/CI and proving a public key really belongs to a teammate is a manual, out-of-band process.</li>
<li><strong>Removing a recipient doesn&rsquo;t un-leak a secret.</strong> <code>sops updatekeys</code> stops <em>future</em> access, but anyone who already decrypted (or kept an old Git revision) still has the plaintext. Off-boarding means <strong>rotating the actual secret values</strong>, not just the keys.</li>
<li><strong>Git history is forever.</strong> If you ever commit a plaintext secret by mistake, it lives in history until you rewrite it. Encrypt <em>before</em> the first commit.</li>
<li><strong>Metadata isn&rsquo;t secret.</strong> Keys, structure, and comments stay in cleartext by design. Don&rsquo;t put sensitive data in a <em>key name</em> like <code>aws_root_password_for_acme_corp</code>. The value lengths are also roughly observable.</li>
<li><strong>Per-value encryption only suits structured files.</strong> SOPS shines on YAML/JSON/ENV. For arbitrary binaries it falls back to whole-file encryption, losing the nice diffs — at which point plain <code>age</code> may be simpler.</li>
<li><strong>Decryption needs the private key present at runtime.</strong> In Kubernetes that usually means storing the age key as an in-cluster secret for Flux/Argo to use — so your cluster&rsquo;s secret store becomes the new root of trust you must protect.</li>
<li><strong>No built-in auditing or rotation policy.</strong> Unlike a managed KMS, there&rsquo;s no access log, automatic rotation, or fine-grained IAM. You build process around it yourself. For high-compliance environments, SOPS can target KMS backends instead of (or alongside) age.</li>
<li><strong>Whitespace/formatting churn.</strong> SOPS rewrites files on encrypt and may normalize formatting, which can produce noisier diffs than expected.</li>
</ul>
<p>None of these are dealbreakers — they&rsquo;re the cost of a serverless, Git-native model. For most teams, homelabs, and GitOps setups, the simplicity is well worth it.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>SOPS + age turns &ldquo;secrets in Git&rdquo; from an oxymoron into a clean workflow.</p>
<p>age gives you small, portable keys and a dead-simple envelope-encryption scheme where public keys safely wrap a file key for any number of recipients.</p>
<p>SOPS layers a structure-aware, per-value editor on top, so your encrypted secrets diff and review like normal config.</p>
<p>Pin both with <code>mise</code>, declare recipients in <code>.sops.yaml</code>, and your secrets become just another reviewable, version-controlled, GitOps-friendly file — with the plaintext staying exactly where it belongs: nowhere near the repo.</p>
<h2 id="useful-links">Useful links</h2>
<ul>
<li><a href="https://github.com/getsops/sops">SOPS — getsops/sops</a></li>
<li><a href="https://github.com/FiloSottile/age">age — FiloSottile/age</a></li>
<li><a href="https://age-encryption.org/v1">age design &amp; specification</a></li>
<li><a href="https://fluxcd.io/flux/guides/mozilla-sops/">Flux: Manage Kubernetes secrets with SOPS</a></li>
<li><a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/">Argo CD secret management</a></li>
<li><a href="../getting-started-with-mise/">Getting started with mise</a></li>
</ul>
]]></content:encoded></item><item><title>Getting Started With mise: Dev Tools, Environments, and Tasks</title><link>https://blog.canhdinh.com/posts/getting-started-with-mise/</link><pubDate>Fri, 26 Jun 2026 20:45:36 +0700</pubDate><guid>https://blog.canhdinh.com/posts/getting-started-with-mise/</guid><description>&lt;p&gt;If you have ever juggled &lt;code&gt;nvm&lt;/code&gt;, &lt;code&gt;pyenv&lt;/code&gt;, &lt;code&gt;rbenv&lt;/code&gt;, a pile of &lt;code&gt;.env&lt;/code&gt; files, and a &lt;code&gt;Makefile&lt;/code&gt; in the same project, &lt;a href="https://mise.jdx.dev/"&gt;&lt;code&gt;mise&lt;/code&gt;&lt;/a&gt; (pronounced &amp;ldquo;meez&amp;rdquo;, short for &lt;em&gt;mise-en-place&lt;/em&gt;) is worth a look.&lt;/p&gt;
&lt;p&gt;It replaces all of them with a single tool and a single &lt;code&gt;mise.toml&lt;/code&gt; config file.&lt;/p&gt;
&lt;p&gt;In this post I&amp;rsquo;ll introduce the three major concepts that make &lt;code&gt;mise&lt;/code&gt; useful day to day:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://mise.jdx.dev/dev-tools/"&gt;Dev tools&lt;/a&gt; — install and switch between runtimes like Node, Python, and Go.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mise.jdx.dev/environments/"&gt;Environments&lt;/a&gt; — load the right environment variables per directory.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mise.jdx.dev/tasks/"&gt;Tasks&lt;/a&gt; — a built-in task runner for builds, tests, linting, and scripts.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then I&amp;rsquo;ll walk through a small but fully runnable example that ties all three together.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you have ever juggled <code>nvm</code>, <code>pyenv</code>, <code>rbenv</code>, a pile of <code>.env</code> files, and a <code>Makefile</code> in the same project, <a href="https://mise.jdx.dev/"><code>mise</code></a> (pronounced &ldquo;meez&rdquo;, short for <em>mise-en-place</em>) is worth a look.</p>
<p>It replaces all of them with a single tool and a single <code>mise.toml</code> config file.</p>
<p>In this post I&rsquo;ll introduce the three major concepts that make <code>mise</code> useful day to day:</p>
<ol>
<li><a href="https://mise.jdx.dev/dev-tools/">Dev tools</a> — install and switch between runtimes like Node, Python, and Go.</li>
<li><a href="https://mise.jdx.dev/environments/">Environments</a> — load the right environment variables per directory.</li>
<li><a href="https://mise.jdx.dev/tasks/">Tasks</a> — a built-in task runner for builds, tests, linting, and scripts.</li>
</ol>
<p>Then I&rsquo;ll walk through a small but fully runnable example that ties all three together.</p>
<h2 id="what-is-mise">What is mise?</h2>
<p><code>mise</code> is a polyglot tool manager, environment manager, and task runner rolled into one binary.</p>
<p>It is a spiritual successor to <a href="https://mise.jdx.dev/dev-tools/comparison-to-asdf.html">asdf</a> (and is compatible with asdf&rsquo;s <code>.tool-versions</code> files), but written in Rust and considerably faster.</p>
<p>The core idea: you declare what a project needs in a <code>mise.toml</code> file, and <code>mise</code> makes sure those tools, versions, and environment variables are active whenever you&rsquo;re inside that directory.</p>
<h2 id="install-mise">Install mise</h2>
<p>On Linux or macOS:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl https://mise.run <span class="p">|</span> sh
</span></span></code></pre></td></tr></table>
</div>
</div><p>By default <code>mise</code> installs to <code>~/.local/bin</code>. Verify it:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">~/.local/bin/mise --version
</span></span><span class="line"><span class="cl"><span class="c1"># mise 2026.x.x</span>
</span></span></code></pre></td></tr></table>
</div>
</div><blockquote>
<p>See the <a href="https://mise.jdx.dev/installing-mise.html">installation guide</a> for other methods: Homebrew, <code>apt</code>, <code>dnf</code>, Snap, Nix, Windows, and more.</p>
</blockquote>
<h3 id="activate-mise-recommended">Activate mise (recommended)</h3>
<p><code>mise exec</code> works for one-off commands, but for interactive shells you&rsquo;ll want to <em>activate</em> <code>mise</code> so tools and env vars load automatically as you <code>cd</code> around.</p>
<p>Add the appropriate line to your shell&rsquo;s rc file:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># bash</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;eval &#34;$(~/.local/bin/mise activate bash)&#34;&#39;</span> &gt;&gt; ~/.bashrc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># zsh</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;eval &#34;$(~/.local/bin/mise activate zsh)&#34;&#39;</span> &gt;&gt; ~/.zshrc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># fish</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;~/.local/bin/mise activate fish | source&#39;</span> &gt;&gt; ~/.config/fish/config.fish
</span></span></code></pre></td></tr></table>
</div>
</div><p>Restart your shell, then run a health check:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise doctor
</span></span></code></pre></td></tr></table>
</div>
</div><blockquote>
<p>For CI/CD, IDEs, and non-interactive scripts, <code>mise</code> also supports <a href="https://mise.jdx.dev/dev-tools/shims.html">shims</a> instead of shell activation.</p>
</blockquote>
<h2 id="concept-1-dev-tools">Concept 1: Dev tools</h2>
<p><code>mise</code> manages multiple versions of programming language runtimes and CLI tools on the same machine, then switches between them automatically based on the directory you&rsquo;re in.</p>
<p>Tools are declared in the <code>[tools]</code> section of <code>mise.toml</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">node</span> <span class="p">=</span> <span class="s1">&#39;24&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">python</span> <span class="p">=</span> <span class="s1">&#39;3&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">go</span> <span class="p">=</span> <span class="s1">&#39;latest&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You rarely write this by hand. The <code>mise use</code> command adds tools for you and installs them:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># add to the current project&#39;s mise.toml</span>
</span></span><span class="line"><span class="cl">mise use node@24
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># install globally (~/.config/mise/config.toml)</span>
</span></span><span class="line"><span class="cl">mise use --global node@24
</span></span></code></pre></td></tr></table>
</div>
</div><p>To run a tool once without installing it permanently, use <code>mise exec</code> (aliased to <code>mise x</code>):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">exec</span> node@26 -- node -v
</span></span><span class="line"><span class="cl"><span class="c1"># downloads node 26 if needed, then prints v26.x.x</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="how-version-switching-works">How version switching works</h3>
<p>Once <code>mise</code> is activated, it walks up the directory tree looking for config files (<code>mise.toml</code>, <code>.tool-versions</code>, <code>.node-version</code>, etc.), resolves the requested versions, and prepends the correct binaries to your <code>PATH</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># inside a project pinned to node 20</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nv">$PATH</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /home/user/.local/share/mise/installs/node/20.x.x/bin:/usr/local/bin:...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">node -v
</span></span><span class="line"><span class="cl"><span class="c1"># v20.x.x</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Move to another project pinned to a different version and <code>node</code> changes automatically.</p>
<p>Tools come from multiple <a href="https://mise.jdx.dev/dev-tools/backend_architecture.html">backends</a> — core, <code>aqua</code>, <code>npm</code>, <code>pipx</code>, <code>cargo</code>, <code>github</code>, and asdf plugins — so the same <code>mise use</code> workflow installs almost anything. Browse the <a href="https://mise.jdx.dev/registry.html">registry</a> to see what&rsquo;s available.</p>
<h2 id="concept-2-environments">Concept 2: Environments</h2>
<p><code>mise</code> can load environment variables per project from the <code>[env]</code> section of <code>mise.toml</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">env</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">NODE_ENV</span> <span class="p">=</span> <span class="s1">&#39;production&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">APP_PORT</span> <span class="p">=</span> <span class="s1">&#39;3000&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>When <code>mise</code> is activated, these are set automatically when you <code>cd</code> into the project and unset when you leave. You can also manage them from the CLI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise <span class="nb">set</span> <span class="nv">NODE_ENV</span><span class="o">=</span>development
</span></span><span class="line"><span class="cl">mise <span class="nb">set</span>
</span></span><span class="line"><span class="cl"><span class="c1"># key       value        source</span>
</span></span><span class="line"><span class="cl"><span class="c1"># NODE_ENV  development  mise.toml</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">mise <span class="nb">unset</span> NODE_ENV
</span></span></code></pre></td></tr></table>
</div>
</div><p>A few handy features:</p>
<ul>
<li>
<p><strong>Unset a variable</strong> inherited from a parent config by setting it to <code>false</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<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">env</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">NODE_ENV</span> <span class="p">=</span> <span class="kc">false</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>Provide a fallback</strong> that only applies if the variable isn&rsquo;t already set, using <code>default</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<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">env</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">NODE_ENV</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">default</span> <span class="p">=</span> <span class="s2">&#34;development&#34;</span> <span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>Load a dotenv file</strong> with the <code>_.file</code> directive:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<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">env</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">_</span><span class="p">.</span><span class="nx">file</span> <span class="p">=</span> <span class="s1">&#39;.env&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>Redact secrets</strong> so they don&rsquo;t leak into task output:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<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">env</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">SECRET</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;my_secret&#34;</span><span class="p">,</span> <span class="nx">redact</span> <span class="p">=</span> <span class="kc">true</span> <span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ul>
<p>Because env vars resolve alongside tools, you get a single source of truth for &ldquo;what does this project need to run&rdquo; — versions <em>and</em> configuration together.</p>
<h2 id="concept-3-tasks">Concept 3: Tasks</h2>
<p><code>mise</code> has a built-in task runner, so you can replace a <code>Makefile</code> or a pile of npm scripts. Tasks run with the full <code>mise</code> context loaded — your tools and env vars are already on <code>PATH</code>.</p>
<p>Define tasks in the <code>[tasks]</code> section:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">build</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Build the project&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;go build ./...&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">test</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run the test suite&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;go test ./...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">depends</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;build&#34;</span><span class="p">]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Run them with <code>mise run</code> (aliased to <code>mise r</code>):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise run build
</span></span><span class="line"><span class="cl">mise run <span class="nb">test</span>     <span class="c1"># runs build first because of `depends`</span>
</span></span><span class="line"><span class="cl">mise tasks        <span class="c1"># list all available tasks</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="task-dependencies">Task dependencies</h3>
<p>The <code>depends</code> array is what turns a list of tasks into a build graph.</p>
<p>When you run a task, <code>mise</code> first runs everything in its <code>depends</code> list, and because dependencies execute <strong>in parallel by default</strong>, independent steps don&rsquo;t block each other:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">lint</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;golangci-lint run ./...&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">test</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;go test ./...&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">ci</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run lint and test, then build&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">depends</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;lint&#34;</span><span class="p">,</span> <span class="s2">&#34;test&#34;</span><span class="p">]</span>   <span class="c"># these two run concurrently</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;go build ./...&#34;</span>        <span class="c"># runs only after both succeed</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Running <code>mise run ci</code> executes <code>lint</code> and <code>test</code> at the same time, waits for both to finish, and only then runs <code>ci</code>&rsquo;s own <code>run</code> command.</p>
<p>There are related keys too — <code>depends_post</code> for cleanup steps that run afterwards, and <code>wait_for</code> for soft ordering without forcing a task to run. See <a href="https://mise.jdx.dev/tasks/task-configuration.html">task dependencies</a> for the full list.</p>
<p>Some other highlights:</p>
<ul>
<li>
<p><strong>File watching</strong> with <code>mise watch</code> to rerun a task when sources change.</p>
</li>
<li>
<p><strong>File tasks</strong> — instead of inlining shell into TOML strings, you can drop an executable script into a <code>mise-tasks/</code> directory and get proper syntax highlighting and linting:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="c1">#MISE description=&#34;Build the CLI&#34;</span>
</span></span><span class="line"><span class="cl">go build ./...
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ul>
<p>Tasks also receive useful variables like <code>MISE_PROJECT_ROOT</code> and <code>MISE_TASK_NAME</code>, so scripts can be location-independent.</p>
<h2 id="a-runnable-example">A runnable example</h2>
<p>Let&rsquo;s combine all three concepts into one small Go web project you can actually run. We&rsquo;ll also add <a href="https://golangci-lint.run/"><code>golangci-lint</code></a> as an additional tool to show how <code>mise</code> manages more than just language runtimes.</p>
<h3 id="1-create-the-project">1. Create the project</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir mise-demo <span class="o">&amp;&amp;</span> <span class="nb">cd</span> mise-demo
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="2-add-the-tools">2. Add the tools</h3>
<p>Pin the exact Go version for this project. This creates <code>mise.toml</code> and installs Go if needed:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise use go@1.26.4
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then add <code>golangci-lint</code> at a specific version. It lives in <code>mise</code>&rsquo;s <a href="https://mise.jdx.dev/registry.html">registry</a>, so the same <code>mise use</code> workflow installs it:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise use golangci-lint@2.12.2
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="3-configure-environment-and-tasks">3. Configure environment and tasks</h3>
<p>Open the generated <code>mise.toml</code> and make it look like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">settings</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">lockfile</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">go</span> <span class="p">=</span> <span class="s2">&#34;1.26.4&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">golangci-lint</span> <span class="p">=</span> <span class="s2">&#34;2.12.2&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">env</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">APP_NAME</span> <span class="p">=</span> <span class="s2">&#34;mise-demo&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">APP_PORT</span> <span class="p">=</span> <span class="s2">&#34;8080&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">build</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Compile the web server&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;go build -o bin/server .&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">lint</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run golangci-lint&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;golangci-lint run ./...&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">serve</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run the web server&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;go run .&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">ci</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Lint and build (lint runs first)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">depends</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;lint&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;go build -o bin/server .&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>[tasks.ci]</code> task uses <code>depends</code> to guarantee linting happens before the build — a small example of the <a href="#task-dependencies">task dependencies</a> covered above.</p>
<h3 id="4-lock-the-tool-versions-with-miselock">4. Lock the tool versions with <code>mise.lock</code></h3>
<p>We set <code>lockfile = true</code> above, but lockfiles are <strong>not</strong> generated automatically. Run <code>mise lock</code> (or just <code>mise install</code>) to create one:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise lock
</span></span></code></pre></td></tr></table>
</div>
</div><p>This writes a <code>mise.lock</code> file next to <code>mise.toml</code>, pinning the exact resolved versions, checksums, and per-platform download URLs (truncated here for brevity):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># mise.lock</span>
</span></span><span class="line"><span class="cl"><span class="c"># @generated - this file is auto-generated by `mise lock`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">tools</span><span class="p">.</span><span class="nx">go</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="nx">version</span> <span class="p">=</span> <span class="s2">&#34;1.26.4&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">backend</span> <span class="p">=</span> <span class="s2">&#34;core:go&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">.</span><span class="nx">go</span><span class="p">.</span><span class="s2">&#34;platforms.linux-x64&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">checksum</span> <span class="p">=</span> <span class="s2">&#34;sha256:1153d3d50e0ac764b447adfe05c2bcf08e889d42a02e0fe0259bd47f6733ad7f&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">url</span> <span class="p">=</span> <span class="s2">&#34;https://dl.google.com/go/go1.26.4.linux-amd64.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">tools</span><span class="p">.</span><span class="nx">golangci-lint</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="nx">version</span> <span class="p">=</span> <span class="s2">&#34;2.12.2&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">backend</span> <span class="p">=</span> <span class="s2">&#34;aqua:golangci/golangci-lint&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">.</span><span class="nx">golangci-lint</span><span class="p">.</span><span class="s2">&#34;platforms.linux-x64&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">checksum</span> <span class="p">=</span> <span class="s2">&#34;sha256:8df580d2670fed8fa984aac0507099af8df275e665215f5c7a2ae3943893a553&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">url</span> <span class="p">=</span> <span class="s2">&#34;https://github.com/golangci/golangci-lint/releases/download/v2.12.2/golangci-lint-2.12.2-linux-amd64.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">provenance</span> <span class="p">=</span> <span class="s2">&#34;github-attestations&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Commit <code>mise.lock</code> alongside <code>mise.toml</code>. Now even loose specs (like <code>go = &quot;1&quot;</code>) resolve to the locked version on every machine, giving you reproducible installs.</p>
<p>In CI you can go a step further with the <a href="https://mise.jdx.dev/configuration/settings.html#locked"><code>locked</code></a> setting (or <code>mise install --locked</code>) to fail fast if the lockfile is missing or incomplete.</p>
<h3 id="5-add-the-application-code">5. Add the application code</h3>
<p>Initialize the Go module:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go mod init mise-demo
</span></span></code></pre></td></tr></table>
</div>
</div><p>Create <code>main.go</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;fmt&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;log&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;net/http&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;os&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;runtime&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">name</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">envOr</span><span class="p">(</span><span class="s">&#34;APP_NAME&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;unknown&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">port</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">envOr</span><span class="p">(</span><span class="s">&#34;APP_PORT&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;8080&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;Hello from %q, served by Go %s\n&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">runtime</span><span class="p">.</span><span class="nf">Version</span><span class="p">());</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;write response: %v&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">addr</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;:&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">port</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;%s listening on http://localhost%s&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">addr</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="nx">addr</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">envOr</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">fallback</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">v</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">fallback</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Note that the handler checks the error returned by <code>fmt.Fprintf</code>. This isn&rsquo;t just good practice.</p>
<p>Without it, <code>golangci-lint</code>&rsquo;s default <code>errcheck</code> linter would flag the unchecked return value and the <code>ci</code> task would fail at the lint step.</p>
<h3 id="6-run-it">6. Run it</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># confirm the tools are active</span>
</span></span><span class="line"><span class="cl">mise <span class="nb">exec</span> -- go version
</span></span><span class="line"><span class="cl"><span class="c1"># go version go1.26.4 ...</span>
</span></span><span class="line"><span class="cl">mise <span class="nb">exec</span> -- golangci-lint version
</span></span><span class="line"><span class="cl"><span class="c1"># golangci-lint has version 2.12.2 ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># list the tasks mise discovered</span>
</span></span><span class="line"><span class="cl">mise tasks
</span></span><span class="line"><span class="cl"><span class="c1"># build  Compile the web server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ci     Lint and build (lint runs first)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># lint   Run golangci-lint</span>
</span></span><span class="line"><span class="cl"><span class="c1"># serve  Run the web server</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># lint then build in one shot (lint runs first via `depends`)</span>
</span></span><span class="line"><span class="cl">mise run ci
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># start the server (uses APP_PORT from [env])</span>
</span></span><span class="line"><span class="cl">mise run serve
</span></span><span class="line"><span class="cl"><span class="c1"># mise-demo listening on http://localhost:8080</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>In another terminal:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl localhost:8080
</span></span><span class="line"><span class="cl"><span class="c1"># Hello from &#34;mise-demo&#34;, served by Go go1.26.4</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>That&rsquo;s the whole loop. <code>mise.toml</code> declares the <strong>tools</strong> (Go 1.26.4 plus <code>golangci-lint</code> 2.12.2), the <strong>environment</strong> (<code>APP_NAME</code>, <code>APP_PORT</code>), and the <strong>tasks</strong> (<code>build</code>, <code>lint</code>, <code>serve</code>, <code>ci</code>); <code>mise.lock</code> pins exactly what gets installed.</p>
<p>Anyone who clones the repo and runs <code>mise install</code> followed by <code>mise run ci</code> gets the exact same setup — no README full of &ldquo;first install the right Go version, then <code>go install golangci-lint</code>, then&hellip;&rdquo; steps.</p>
<h3 id="7-clean-up-optional">7. Clean up (optional)</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> .. <span class="o">&amp;&amp;</span> rm -rf mise-demo
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="bonus-global-quality-of-life-cli-tools">Bonus: global quality-of-life CLI tools</h2>
<p>So far we&rsquo;ve used <code>mise</code> for per-project toolchains, but it&rsquo;s just as handy for the CLI tools you want available <em>everywhere</em>.</p>
<p>The <code>-g</code> (<code>--global</code>) flag installs a tool and records it in your global config at <code>~/.config/mise/config.toml</code> instead of a project <code>mise.toml</code>.</p>
<p>Here&rsquo;s a starter kit of quality-of-life tools, each pulled from <code>mise</code>&rsquo;s <a href="https://mise.jdx.dev/registry.html">registry</a> across different backends (core, aqua, npm, GitHub releases):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise use -g lazygit@latest    <span class="c1"># terminal UI for git</span>
</span></span><span class="line"><span class="cl">mise use -g fzf@latest        <span class="c1"># fuzzy finder</span>
</span></span><span class="line"><span class="cl">mise use -g ripgrep@latest    <span class="c1"># fast grep (rg)</span>
</span></span><span class="line"><span class="cl">mise use -g fd@latest         <span class="c1"># fast, friendly find</span>
</span></span><span class="line"><span class="cl">mise use -g gh@latest         <span class="c1"># GitHub CLI</span>
</span></span><span class="line"><span class="cl">mise use -g copilot@latest    <span class="c1"># GitHub Copilot CLI</span>
</span></span><span class="line"><span class="cl">mise use -g neovim            <span class="c1"># hyperextensible Vim-based editor</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Each command updates the same global config:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># ~/.config/mise/config.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">lazygit</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">fzf</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">ripgrep</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">fd</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">gh</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">copilot</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">neovim</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Because this file lives in your dotfiles location, you can commit it to your dotfiles repo and reproduce your entire CLI toolbox on a new machine with a single command:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise install
</span></span></code></pre></td></tr></table>
</div>
</div><p>A couple of tips:</p>
<ul>
<li>Run <code>mise ls</code> to see everything <code>mise</code> manages (global and local) and which versions are active.</li>
<li>Pin a tool to a real version instead of <code>latest</code> (e.g. <code>mise use -g fzf@0.56.3</code>) when you want reproducibility over always-newest.</li>
<li>Upgrade everything later with <code>mise upgrade</code>.</li>
</ul>
<h2 id="why-this-matters">Why this matters</h2>
<p>The payoff is reproducibility with almost no ceremony. A single committed <code>mise.toml</code> answers three questions at once:</p>
<ul>
<li>Which tool versions does this project use?</li>
<li>Which environment variables does it expect?</li>
<li>How do I build, test, and run it?</li>
</ul>
<p>New contributors run <code>mise install</code> and they&rsquo;re ready — and because <code>mise.lock</code> pins exact versions, they get the same toolchain you do.</p>
<p>CI runs the same <code>mise run</code> commands as your laptop. And you stop maintaining three separate tools to do one job.</p>
<h2 id="useful-links">Useful links</h2>
<ul>
<li><a href="https://mise.jdx.dev/getting-started.html">mise — Getting Started</a></li>
<li><a href="https://mise.jdx.dev/dev-tools/">Dev Tools</a></li>
<li><a href="https://mise.jdx.dev/environments/">Environments</a></li>
<li><a href="https://mise.jdx.dev/tasks/">Tasks</a></li>
<li><a href="https://mise.jdx.dev/configuration.html">Configuration reference</a></li>
<li><a href="https://mise.jdx.dev/configuration/settings.html#lockfile">Lockfiles (<code>lockfile</code> setting)</a></li>
<li><a href="https://mise.jdx.dev/registry.html">Tool registry</a></li>
<li><a href="https://mise.jdx.dev/dev-tools/comparison-to-asdf.html">Comparison to asdf</a></li>
</ul>
]]></content:encoded></item><item><title>Deploy Static Site With Hugo and Cloudflare Workers</title><link>https://blog.canhdinh.com/posts/deploy-static-site-with-hugo-cloudflare/</link><pubDate>Sun, 21 Jun 2026 18:47:39 +0700</pubDate><guid>https://blog.canhdinh.com/posts/deploy-static-site-with-hugo-cloudflare/</guid><description>&lt;p&gt;This post walks through how I deploy my personal static blog using &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt;, the &lt;a href="https://github.com/adityatelange/hugo-PaperMod"&gt;PaperMod&lt;/a&gt; theme, &lt;a href="https://github.com/"&gt;GitHub&lt;/a&gt;, and &lt;a href="https://developers.cloudflare.com/workers/"&gt;Cloudflare Workers&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on Workers vs. Pages:&lt;/strong&gt; Cloudflare is consolidating static site hosting from Cloudflare Pages into Cloudflare Workers.&lt;/p&gt;
&lt;p&gt;As of 2025, &lt;a href="https://developers.cloudflare.com/workers/static-assets/"&gt;Workers Static Assets&lt;/a&gt; is the recommended way to host new static sites, and Pages is in maintenance mode.&lt;/p&gt;
&lt;p&gt;In the dashboard both still live under the unified &lt;strong&gt;Workers &amp;amp; Pages&lt;/strong&gt; section, which is why the deploy commands below use &lt;code&gt;wrangler deploy&lt;/code&gt; (a Workers command) rather than the older Pages workflow.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post walks through how I deploy my personal static blog using <a href="https://gohugo.io/">Hugo</a>, the <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod</a> theme, <a href="https://github.com/">GitHub</a>, and <a href="https://developers.cloudflare.com/workers/">Cloudflare Workers</a>.</p>
<blockquote>
<p><strong>A note on Workers vs. Pages:</strong> Cloudflare is consolidating static site hosting from Cloudflare Pages into Cloudflare Workers.</p>
<p>As of 2025, <a href="https://developers.cloudflare.com/workers/static-assets/">Workers Static Assets</a> is the recommended way to host new static sites, and Pages is in maintenance mode.</p>
<p>In the dashboard both still live under the unified <strong>Workers &amp; Pages</strong> section, which is why the deploy commands below use <code>wrangler deploy</code> (a Workers command) rather than the older Pages workflow.</p>
</blockquote>
<p>Here is the setup this post builds toward:</p>
<ul>
<li>GitHub repository: <a href="https://github.com/vancanhuit/personal-blog">vancanhuit/personal-blog</a></li>
<li>Live site: <a href="https://blog.canhdinh.com/">blog.canhdinh.com</a></li>
<li>Static site generator: <a href="https://gohugo.io/">Hugo</a></li>
<li>Theme: <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod</a></li>
<li>Hosting/deployment: <a href="https://developers.cloudflare.com/workers/static-assets/">Cloudflare Workers (Static Assets)</a></li>
</ul>
<p>The Cloudflare build and deploy commands are:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule update --init --recursive <span class="o">&amp;&amp;</span> hugo --gc --minify
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx wrangler deploy
</span></span></code></pre></td></tr></table>
</div>
</div><p>The Workers deployment options live in a <code>wrangler.jsonc</code> file committed to the repository (covered later).</p>
<h2 id="why-hugo">Why Hugo?</h2>
<p><a href="https://gohugo.io/">Hugo</a> is a static site generator written in Go. It is fast, simple, and works very well for technical blogs.</p>
<p>A static blog suits my use case because I mostly write posts made up of:</p>
<ul>
<li>Markdown content</li>
<li>Source code examples</li>
<li>DevOps notes</li>
<li>Infrastructure experiments</li>
</ul>
<p>Hugo generates plain HTML, CSS, JavaScript, images, and fonts, so the final site is easy to host on Cloudflare.</p>
<h2 id="why-papermod">Why PaperMod?</h2>
<p>I chose <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod</a> because it is clean, fast, and well suited to technical writing.</p>
<p>The features I wanted are:</p>
<ul>
<li>Clean blog layout</li>
<li>Dark/light mode</li>
<li>Table of contents</li>
<li>Reading time</li>
<li>Tags and categories</li>
<li>Good source code block rendering</li>
<li>Code copy button</li>
<li>Minimal maintenance</li>
</ul>
<p>PaperMod is a practical choice for a technical blog: it stays out of the way and lets the content stand out.</p>
<h2 id="create-the-hugo-site">Create the Hugo site</h2>
<p>Create a new Hugo project. Hugo defaults to a TOML config file, so pass <code>--format yaml</code> to get <code>hugo.yaml</code> instead:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new site personal-blog --format yaml
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> personal-blog
</span></span><span class="line"><span class="cl">git init
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then add PaperMod as a Git submodule:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod
</span></span></code></pre></td></tr></table>
</div>
</div><p>Adding PaperMod as a submodule creates a <code>.gitmodules</code> file in the repository.</p>
<p>Check it with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat .gitmodules
</span></span></code></pre></td></tr></table>
</div>
</div><p>Example:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[submodule &#34;themes/PaperMod&#34;]</span>
</span></span><span class="line"><span class="cl">  <span class="na">path</span> <span class="o">=</span> <span class="s">themes/PaperMod
</span></span></span><span class="line"><span class="cl"><span class="s">  url = https://github.com/adityatelange/hugo-PaperMod</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="configure-hugo">Configure Hugo</h2>
<p>Here is a simplified <code>hugo.yaml</code> configuration:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">baseURL</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://blog.canhdinh.com/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">locale</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;en-us&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Canh Dinh&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">theme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;PaperMod&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">pagination</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pagerSize</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">outputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">home</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">HTML</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">RSS</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">JSON</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;production&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Notes on DevOps practices, self-hosting and software delivery.&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">author</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Canh Dinh&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mainSections</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">posts</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">homeInfoParams</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Hi there 👋&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Content</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      Welcome to my blog.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">defaultTheme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;auto&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">disableThemeToggle</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowReadingTime</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowShareButtons</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowPostNavLinks</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowBreadCrumbs</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowCodeCopyButtons</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowWordCount</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowRssButtonInSectionTermList</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ShowToc</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">TocOpen</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">UseHugoToc</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">socialIcons</span><span class="p">:</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="s2">&#34;github&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://github.com/vancanhuit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">menu</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">main</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">identifier</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;tags&#34;</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="s2">&#34;Tags&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/tags/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">weight</span><span class="p">:</span><span class="w"> </span><span class="m">20</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">identifier</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;archives&#34;</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="s2">&#34;Archives&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/archives/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">weight</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">identifier</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;search&#34;</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="s2">&#34;Search&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/search/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">weight</span><span class="p">:</span><span class="w"> </span><span class="m">40</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">markup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">highlight</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">codeFences</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">guessSyntax</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">lineNos</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">lineNumbersInTable</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">noClasses</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>The two most important values are the production domain and the theme:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">baseURL</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://blog.canhdinh.com/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">theme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;PaperMod&#34;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>A few PaperMod-specific options are worth highlighting:</p>
<ul>
<li><code>mainSections: [posts]</code> makes the home page (<code>/</code>) list posts from <code>content/posts/</code>, so the landing page doubles as the posts archive.</li>
<li><code>homeInfoParams</code> renders an intro block (title plus content) above the post list on the home page.</li>
<li><code>outputs.home</code> adds a <code>JSON</code> output, which generates the search index used by the search page.</li>
</ul>
<h2 id="home-archives-and-search-pages">Home, archives, and search pages</h2>
<p>PaperMod renders the post list on the home page automatically, but the <strong>Archives</strong> and <strong>Search</strong> pages each need a content file that selects the right layout. Without them, the menu links return <code>404</code>.</p>
<p>Create the archives page:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new content archives.md
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">title: &#34;Archives&#34;
</span></span><span class="line"><span class="cl">layout: &#34;archives&#34;
</span></span><span class="line"><span class="cl">url: &#34;/archives/&#34;
</span></span><span class="line"><span class="cl">summary: &#34;archives&#34;
</span></span><span class="line"><span class="cl">---
</span></span></code></pre></td></tr></table>
</div>
</div><p>Create the search page:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new content search.md
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">title: &#34;Search&#34;
</span></span><span class="line"><span class="cl">layout: &#34;search&#34;
</span></span><span class="line"><span class="cl">url: &#34;/search/&#34;
</span></span><span class="line"><span class="cl">summary: &#34;search&#34;
</span></span><span class="line"><span class="cl">placeholder: &#34;Search posts...&#34;
</span></span><span class="line"><span class="cl">---
</span></span></code></pre></td></tr></table>
</div>
</div><p>The search page relies on the <code>JSON</code> home output configured earlier. PaperMod ships with <a href="https://www.fusejs.io/">Fuse.js</a> and runs fuzzy, client-side search over that index, so no server-side component is required.</p>
<h2 id="add-a-blog-post">Add a blog post</h2>
<p>Create a new post:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new content posts/hello-world.md
</span></span></code></pre></td></tr></table>
</div>
</div><p>Example post:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">title: &#34;Hello World&#34;
</span></span><span class="line"><span class="cl">date: 2026-06-21T10:00:00+07:00
</span></span><span class="line"><span class="cl">draft: false
</span></span><span class="line"><span class="cl">tags: [&#34;hugo&#34;, &#34;cloudflare&#34;, &#34;devops&#34;]
</span></span><span class="line"><span class="cl">categories: [&#34;devops&#34;]
</span></span><span class="line"><span class="cl">showToc: true
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This is my first post using Hugo, PaperMod, GitHub, and Cloudflare.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="s">```go
</span></span></span><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="s">&#34;fmt&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Hello from my blog&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Make sure the post is publishable:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">draft</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>If <code>draft</code> is set to <code>true</code>, Hugo excludes the post from production builds.</p>
<h2 id="run-locally">Run locally</h2>
<p>For local development, run:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo server -D --disableFastRender
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then open:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">http://localhost:1313
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>-D</code> flag includes draft posts.</p>
<p>If the site looks stale after changing CSS, fonts, or theme files, clean the generated output and rebuild:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rm -rf public resources/_gen
</span></span><span class="line"><span class="cl">hugo --gc --minify
</span></span><span class="line"><span class="cl">hugo server -D --disableFastRender
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="push-to-github">Push to GitHub</h2>
<p>Commit and push the project:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add .
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Initial Hugo blog with PaperMod&#34;</span>
</span></span><span class="line"><span class="cl">git branch -M main
</span></span><span class="line"><span class="cl">git remote add origin https://github.com/vancanhuit/personal-blog.git
</span></span><span class="line"><span class="cl">git push -u origin main
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="configure-cloudflare-deployment">Configure Cloudflare deployment</h2>
<p>In the Cloudflare dashboard, open <strong>Workers &amp; Pages</strong>, create a new Worker, connect the GitHub repository, and set the following commands.</p>
<h3 id="build-command">Build command</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule update --init --recursive <span class="o">&amp;&amp;</span> hugo --gc --minify
</span></span></code></pre></td></tr></table>
</div>
</div><p>This does two things:</p>
<ol>
<li>Fetches the PaperMod theme submodule.</li>
<li>Builds and minifies the Hugo site into the <code>public/</code> directory.</li>
</ol>
<p>The <code>git submodule update --init --recursive</code> step matters because PaperMod is stored as a Git submodule.</p>
<h3 id="deploy-command">Deploy command</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx wrangler deploy
</span></span></code></pre></td></tr></table>
</div>
</div><p>The deploy command stays minimal because the Workers options live in a <code>wrangler.jsonc</code> file at the repository root:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;personal-blog&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;compatibility_date&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-06-21&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;assets&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;directory&#34;</span><span class="p">:</span> <span class="s2">&#34;public&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;preview_urls&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;workers_dev&#34;</span><span class="p">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This file tells Wrangler the Worker name, the compatibility date, and the directory (<code>public/</code>) to serve as static assets.</p>
<p>Two settings disable Cloudflare&rsquo;s automatic preview endpoints, since I only want the production domain to serve the site:</p>
<ul>
<li><code>&quot;preview_urls&quot;: false</code> turns off the per-version preview URLs Cloudflare generates for each deployment.</li>
<li><code>&quot;workers_dev&quot;: false</code> disables the default <code>*.workers.dev</code> subdomain, so the Worker is reachable only through my custom domain.</li>
</ul>
<h2 id="environment-variables">Environment variables</h2>
<p>I also recommend setting <code>HUGO_VERSION</code> in Cloudflare:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">HUGO_VERSION = 0.163.3
</span></span></code></pre></td></tr></table>
</div>
</div><p>Pinning the Hugo version avoids unexpected build differences between local and Cloudflare environments.</p>
<h2 id="self-host-fonts">Self-host fonts</h2>
<p>I self-host two fonts so the site does not depend on a third-party font CDN:</p>
<ul>
<li><a href="https://github.com/rsms/inter">Inter</a> for body and UI text.</li>
<li><a href="https://github.com/subframe7536/maple-font">Maple Mono NL</a> for code blocks.</li>
</ul>
<p>Font files are stored under:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">static/fonts/inter/
</span></span><span class="line"><span class="cl">static/fonts/maple-mono/
</span></span></code></pre></td></tr></table>
</div>
</div><p>For example:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">static/fonts/maple-mono/MapleMonoNL-Regular.ttf.woff2
</span></span><span class="line"><span class="cl">static/fonts/maple-mono/MapleMonoNL-Bold.ttf.woff2
</span></span><span class="line"><span class="cl">static/fonts/maple-mono/MapleMonoNL-Italic.ttf.woff2
</span></span></code></pre></td></tr></table>
</div>
</div><p>I then define the code font in:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">assets/css/extended/custom-fonts.css
</span></span></code></pre></td></tr></table>
</div>
</div><p>Example:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">@</span><span class="k">font-face</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-family</span><span class="o">:</span> <span class="s2">&#34;Maple Mono NL&#34;</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">src</span><span class="o">:</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/fonts/maple-mono/MapleMonoNL-Regular.ttf.woff2&#34;</span><span class="o">)</span> <span class="nt">format</span><span class="o">(</span><span class="s2">&#34;woff2&#34;</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-weight</span><span class="o">:</span> <span class="nt">400</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-style</span><span class="o">:</span> <span class="nt">normal</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-display</span><span class="o">:</span> <span class="nt">swap</span><span class="o">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">@</span><span class="k">font-face</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-family</span><span class="o">:</span> <span class="s2">&#34;Maple Mono NL&#34;</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">src</span><span class="o">:</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/fonts/maple-mono/MapleMonoNL-Bold.ttf.woff2&#34;</span><span class="o">)</span> <span class="nt">format</span><span class="o">(</span><span class="s2">&#34;woff2&#34;</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-weight</span><span class="o">:</span> <span class="nt">700</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-style</span><span class="o">:</span> <span class="nt">normal</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-display</span><span class="o">:</span> <span class="nt">swap</span><span class="o">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">code</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="nt">pre</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="nt">kbd</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="nt">samp</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">highlight</span> <span class="nt">pre</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">highlight</span> <span class="nt">code</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">chroma</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">chroma</span> <span class="nt">code</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Maple Mono NL&#34;</span><span class="p">,</span> <span class="n">ui-monospace</span><span class="p">,</span> <span class="n">SFMono-Regular</span><span class="p">,</span> <span class="n">Menlo</span><span class="p">,</span> <span class="n">Monaco</span><span class="p">,</span> <span class="n">Consolas</span><span class="p">,</span> <span class="s2">&#34;Liberation Mono&#34;</span><span class="p">,</span> <span class="kc">monospace</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">font-feature-settings</span><span class="p">:</span> <span class="s2">&#34;liga&#34;</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">&#34;calt&#34;</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">font-variant-ligatures</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>I disable ligatures because I prefer code to display exactly as typed.</p>
<p>For body text, I load Inter as a variable font (two <code>woff2</code> files cover every weight) and set it as the base font:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">@</span><span class="k">font-face</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-family</span><span class="o">:</span> <span class="s2">&#34;Inter&#34;</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">src</span><span class="o">:</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/fonts/inter/inter-latin-wght-normal.woff2&#34;</span><span class="o">)</span> <span class="nt">format</span><span class="o">(</span><span class="s2">&#34;woff2-variations&#34;</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-weight</span><span class="o">:</span> <span class="nt">100</span> <span class="nt">900</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-style</span><span class="o">:</span> <span class="nt">normal</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">font-display</span><span class="o">:</span> <span class="nt">swap</span><span class="o">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">body</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Inter&#34;</span><span class="p">,</span> <span class="o">-</span><span class="n">apple-system</span><span class="p">,</span> <span class="n">BlinkMacSystemFont</span><span class="p">,</span> <span class="s2">&#34;Segoe UI&#34;</span><span class="p">,</span> <span class="n">Roboto</span><span class="p">,</span> <span class="n">Helvetica</span><span class="p">,</span> <span class="n">Arial</span><span class="p">,</span> <span class="kc">sans-serif</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Because the extended stylesheet loads after the theme&rsquo;s CSS, this <code>body</code> rule overrides PaperMod&rsquo;s default font stack.</p>
<h2 id="custom-styling">Custom styling</h2>
<p>PaperMod automatically bundles any CSS placed in <code>assets/css/extended/</code>, loaded after the theme styles. I use a <code>custom.css</code> file there to make the content stand out and improve reading comfort:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">assets/css/extended/custom.css
</span></span></code></pre></td></tr></table>
</div>
</div><p>The main tweaks are:</p>
<ul>
<li>An accent color for in-content links, with the default underline removed and an underline that animates in on hover.</li>
<li>Styled blockquotes with an accent border, a tinted background, and a decorative quote mark.</li>
<li>Subtle borders on inline code, rounded tables with a shaded header row, and a slim centered horizontal rule.</li>
<li>A softer light palette, brighter dark-mode text, and a larger line height for more comfortable reading.</li>
</ul>
<p>All colors are driven by CSS variables, including PaperMod&rsquo;s own <code>--theme</code>, <code>--content</code>, and <code>--border</code>, plus an <code>--accent</code> variable defined separately for light and <code>.dark</code> modes, so the styling adapts to both themes:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">:</span><span class="nd">root</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nv">--accent</span><span class="p">:</span> <span class="mh">#2563eb</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">dark</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nv">--accent</span><span class="p">:</span> <span class="mh">#6ea8fe</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">post-content</span> <span class="nt">a</span><span class="p">:</span><span class="nd">not</span><span class="o">(</span><span class="p">.</span><span class="nc">anchor</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">accent</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">text-decoration</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">post-content</span> <span class="nt">blockquote</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">border-inline-start</span><span class="p">:</span> <span class="mf">0.25</span><span class="kt">rem</span> <span class="kc">solid</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">accent</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">quote</span><span class="o">-</span><span class="n">bg</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>PaperMod underlines content links via <code>.md-content a:not(.anchor)</code>. That selector has higher specificity than a plain <code>.post-content a</code>, so the override matches it with <code>.post-content a:not(.anchor)</code> to actually remove the underline.</p>
<h2 id="normal-publishing-workflow">Normal publishing workflow</h2>
<p>After the initial setup, publishing a new post is simple.</p>
<p>Create a post:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new content posts/my-new-post.md
</span></span></code></pre></td></tr></table>
</div>
</div><p>Preview locally:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo server -D --disableFastRender
</span></span></code></pre></td></tr></table>
</div>
</div><p>When ready, commit and push:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add .
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Add new blog post&#34;</span>
</span></span><span class="line"><span class="cl">git push
</span></span></code></pre></td></tr></table>
</div>
</div><p>Cloudflare then rebuilds and redeploys the site automatically.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="theme-is-missing">Theme is missing</h3>
<p>If Cloudflare cannot find PaperMod, make sure the theme submodule is committed:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule status
</span></span><span class="line"><span class="cl">cat .gitmodules
</span></span></code></pre></td></tr></table>
</div>
</div><p>Also make sure the build command includes:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule update --init --recursive
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="code-blocks-look-broken">Code blocks look broken</h3>
<p>Clean Hugo&rsquo;s generated files locally, then rebuild:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rm -rf public resources/_gen
</span></span><span class="line"><span class="cl">hugo --gc --minify
</span></span><span class="line"><span class="cl">hugo server -D --disableFastRender
</span></span></code></pre></td></tr></table>
</div>
</div><p>Also check the Markdown code fence format:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="s">```go
</span></span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;hello&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="new-font-does-not-appear">New font does not appear</h3>
<p>Hard refresh the browser:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Ctrl + Shift + R
</span></span></code></pre></td></tr></table>
</div>
</div><p>On macOS:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Cmd + Shift + R
</span></span></code></pre></td></tr></table>
</div>
</div><p>If fonts are cached aggressively, rename the font folder or font files and update the CSS paths.</p>
<h3 id="deployment-fails-after-successful-build">Deployment fails after successful build</h3>
<p>If the log shows Hugo built successfully but deployment failed, check the deploy command and the <code>wrangler.jsonc</code> file.</p>
<p>For this setup, use:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx wrangler deploy
</span></span></code></pre></td></tr></table>
</div>
</div><p>And make sure <code>wrangler.jsonc</code> points the assets directory at the Hugo output:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;personal-blog&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;compatibility_date&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-06-21&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;assets&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;directory&#34;</span><span class="p">:</span> <span class="s2">&#34;public&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;preview_urls&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;workers_dev&#34;</span><span class="p">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If <code>wrangler.jsonc</code> is missing or <code>assets.directory</code> is wrong, Wrangler has nothing to upload and the deploy fails even though the build succeeded.</p>
<h2 id="useful-links">Useful links</h2>
<ul>
<li><a href="https://gohugo.io/">Hugo official website</a></li>
<li><a href="https://gohugo.io/documentation/">Hugo documentation</a></li>
<li><a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod GitHub repository</a></li>
<li><a href="https://github.com/adityatelange/hugo-PaperMod/wiki/Installation">PaperMod installation guide</a></li>
<li><a href="https://developers.cloudflare.com/workers/">Cloudflare Workers</a></li>
<li><a href="https://developers.cloudflare.com/workers/static-assets/">Cloudflare Workers static assets</a></li>
<li><a href="https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/">Migrate from Pages to Workers</a></li>
<li><a href="https://developers.cloudflare.com/workers/wrangler/commands/#deploy">Wrangler deploy command</a></li>
</ul>
<h2 id="final-setup">Final setup</h2>
<p>The final setup is:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Hugo static site
</span></span><span class="line"><span class="cl">PaperMod theme
</span></span><span class="line"><span class="cl">GitHub repository
</span></span><span class="line"><span class="cl">Cloudflare build command:
</span></span><span class="line"><span class="cl">  git submodule update --init --recursive &amp;&amp; hugo --gc --minify
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Cloudflare deploy command:
</span></span><span class="line"><span class="cl">  npx wrangler deploy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Workers options stored in wrangler.jsonc
</span></span></code></pre></td></tr></table>
</div>
</div>]]></content:encoded></item></channel></rss>