<?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>Cloudflare Pages on Chunhao Zhang</title>
    <link>https://blog-6sm.pages.dev/en/tags/cloudflare-pages/</link>
    <description>Recent content in Cloudflare Pages on Chunhao Zhang</description>
    <image>
      <title>Chunhao Zhang</title>
      <url>https://blog-6sm.pages.dev/images/og-default.png</url>
      <link>https://blog-6sm.pages.dev/images/og-default.png</link>
    </image>
    <generator>Hugo</generator>
    <language>en</language>
    <copyright>2026</copyright>
    <lastBuildDate>Wed, 08 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://blog-6sm.pages.dev/en/tags/cloudflare-pages/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Building a Blog with Hugo &#43; Cloudflare Pages</title>
      <link>https://blog-6sm.pages.dev/en/posts/build-blog-with-hugo/</link>
      <pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://blog-6sm.pages.dev/en/posts/build-blog-with-hugo/</guid>
      <description>A walkthrough of building a bilingual tech blog from scratch with Hugo, PaperMod theme, and Cloudflare Pages — including the pitfalls I hit along the way.</description>
      <content:encoded><![CDATA[<p>I&rsquo;d been telling myself I&rsquo;d start a blog for months. Then one afternoon I decided to just do it — no more planning, no more comparing frameworks. A few hours later the site was live. This post is a record of that process: not a tutorial, more of an annotated changelog of mistakes and decisions.</p>
<h2 id="why-hugo">Why Hugo</h2>
<p>I didn&rsquo;t spend long choosing a static site generator. I&rsquo;d used Hexo before and knew what Node.js dependency hell feels like. Jekyll is slow. Gatsby is overkill for a blog. Hugo is a single Go binary — <code>brew install hugo</code> and you&rsquo;re done. No <code>node_modules</code>, no dependency conflicts, and builds so fast you barely notice them happening.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install hugo
</span></span><span class="line"><span class="cl">hugo version
</span></span><span class="line"><span class="cl"><span class="c1"># hugo v0.160.0+extended darwin/arm64</span>
</span></span></code></pre></div><p>That&rsquo;s the entire install. The <code>extended</code> version includes built-in SCSS compilation, which the theme needs.</p>
<p>For the theme, I went with <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod</a>. It&rsquo;s clean, fast, and ships with everything I need out of the box: dark mode, search, table of contents, syntax highlighting, multilingual support. Added it as a git submodule so updates are just a <code>git pull</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new site blog <span class="o">&amp;&amp;</span> <span class="nb">cd</span> blog
</span></span><span class="line"><span class="cl">git init
</span></span><span class="line"><span class="cl">git submodule add --depth<span class="o">=</span><span class="m">1</span> https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
</span></span></code></pre></div><h2 id="configuration-one-yaml-file-does-most-of-the-work">Configuration: One YAML File Does Most of the Work</h2>
<p>Hugo keeps everything in a single <code>hugo.yaml</code> (also supports toml and json — I prefer yaml). My blog is bilingual: Chinese as the default language at the root path, English under <code>/en/</code>. The language config looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">defaultContentLanguage</span><span class="p">:</span><span class="w"> </span><span class="l">zh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">defaultContentLanguageInSubdir</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">  </span><span class="c"># Chinese gets no prefix</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">languages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">zh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">languageName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;中文&#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">1</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;Forest&#39;s Blog&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><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">author</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;北海&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">en</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">languageName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;EN&#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">2</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;Forest&#39;s Blog&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><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">author</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Chunhao Zhang&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Bilingual articles use filename suffixes: Chinese is <code>build-blog-with-hugo.md</code>, English is <code>build-blog-with-hugo.en.md</code>. Hugo automatically links them together for language switching.</p>
<p>Though that &ldquo;automatic linking&rdquo; had a catch — more on that below.</p>
<h2 id="deploying-to-cloudflare-pages">Deploying to Cloudflare Pages</h2>
<p>I chose Cloudflare Pages over GitHub Pages for two reasons: GitHub is unreliable from mainland China, and CF Pages has global CDN nodes with a simpler deploy workflow — just push code and it builds automatically.</p>
<p>But Cloudflare&rsquo;s Dashboard design confused me for a bit.</p>
<p>After clicking into &ldquo;Workers &amp; Pages&rdquo;, the default view shows Workers (Serverless Functions). The Pages entry is buried at the bottom of the page as an unassuming &ldquo;Looking to deploy Pages? Get started&rdquo; link. I spent a few minutes thinking Pages had been merged into Workers before I spotted it.</p>
<p>Once past that, setup was straightforward: connect GitHub repo, select Hugo preset, build command <code>hugo --minify</code>, output directory <code>public</code>. There&rsquo;s one <strong>critical environment variable</strong> you must set manually:</p>
<table>
  <thead>
      <tr>
          <th>Variable</th>
          <th>Value</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>HUGO_VERSION</code></td>
          <td><code>0.160.0</code></td>
          <td>CF&rsquo;s default Hugo version is outdated and will break the build</td>
      </tr>
  </tbody>
</table>
<p>After deployment I got a <code>blog-6sm.pages.dev</code> domain. Custom domains can be added later, but the free subdomain works fine for now.</p>
<h2 id="the-language-switcher-bug">The Language Switcher Bug</h2>
<p>After going live, I noticed an annoying bug: clicking the language toggle (中文 ↔ EN) on any article page would redirect to the homepage instead of the translated version of that article.</p>
<p>Digging into PaperMod&rsquo;s source, I found that the default <code>header.html</code> template uses <code>site.Home.Translations</code> for the language switcher — meaning it always points to the homepage translation, not the current page&rsquo;s translation.</p>
<p>The fix was to create an override <code>header.html</code> in the project&rsquo;s <code>layouts/partials/</code> directory. Hugo&rsquo;s template loading priority is project &gt; theme, so you just copy the theme&rsquo;s header and change one line:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="nx">$translations</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="na">.Translations</span><span class="w"> </span><span class="cp">}}</span>
</span></span><span class="line"><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="k">not</span><span class="w"> </span><span class="nx">$translations</span><span class="w"> </span><span class="cp">}}</span>
</span></span><span class="line"><span class="cl">  <span class="cp">{{-</span><span class="w"> </span><span class="nx">$translations</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">site</span><span class="na">.Home.Translations</span><span class="w"> </span><span class="cp">}}</span>
</span></span><span class="line"><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="k">end</span><span class="w"> </span><span class="cp">}}</span>
</span></span></code></pre></div><p>Prefer the current page&rsquo;s <code>.Translations</code>, fall back to homepage only when there&rsquo;s no translation available. Small change, but this is genuinely a design flaw in PaperMod — on a multilingual site, switching languages on an article page and landing on the homepage feels broken.</p>
<h2 id="comments-with-giscus">Comments with Giscus</h2>
<p>For comments I went with <a href="https://giscus.app/">Giscus</a>, which is powered by GitHub Discussions. No backend needed — comments live in the repo&rsquo;s Discussions, which is the most natural setup for an open-source blog.</p>
<p>One non-obvious config choice: the Discussion category should be <strong>Announcements</strong>. This category restricts who can create new Discussions (only repo admins — corresponding to initializing a comment thread for an article), while still letting anyone reply. If you pick an open category like General, anyone can create arbitrary Discussions and things get messy.</p>
<p>Giscus also needs dark mode adaptation. I used a <code>MutationObserver</code> in <code>comments.html</code> to watch for class changes on <code>body</code> and update the Giscus iframe theme dynamically:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">isDark</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="s1">&#39;dark&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">theme</span> <span class="o">=</span> <span class="nx">isDark</span> <span class="o">?</span> <span class="s1">&#39;noborder_dark&#39;</span> <span class="o">:</span> <span class="s1">&#39;noborder_light&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">iframe</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;iframe.giscus-frame&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">iframe</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">iframe</span><span class="p">.</span><span class="nx">contentWindow</span><span class="p">.</span><span class="nx">postMessage</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span> <span class="nx">giscus</span><span class="o">:</span> <span class="p">{</span> <span class="nx">setConfig</span><span class="o">:</span> <span class="p">{</span> <span class="nx">theme</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;https://giscus.app&#39;</span>
</span></span><span class="line"><span class="cl">        <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="p">});</span>
</span></span><span class="line"><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">attributes</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">attributeFilter</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;class&#39;</span><span class="p">]</span> <span class="p">});</span>
</span></span></code></pre></div><p>This way, toggling dark mode updates the comment section in real time — no jarring white comment box on a dark page.</p>
<h2 id="miscellaneous-setup">Miscellaneous Setup</h2>
<p><strong>KaTeX math rendering</strong> is conditionally loaded. Only articles with <code>math: true</code> in frontmatter pull in the KaTeX CSS and JS, so other pages aren&rsquo;t slowed down.</p>
<p><strong>robots.txt</strong> explicitly allows AI crawlers (GPTBot, ClaudeBot, PerplexityBot). More and more sites are blocking AI bots by default, but for a technical blog that wants to be cited by both humans and AI systems, open crawling makes sense.</p>
<p><strong>Syntax highlighting</strong> uses Hugo&rsquo;s built-in Chroma engine with the <code>dracula</code> theme. PaperMod includes a code copy button out of the box.</p>
<h2 id="the-final-structure">The Final Structure</h2>
<p>Here&rsquo;s what the project looks like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">blog/
</span></span><span class="line"><span class="cl">├── hugo.yaml                    # Where all config lives
</span></span><span class="line"><span class="cl">├── content/
</span></span><span class="line"><span class="cl">│   ├── posts/                   # Articles
</span></span><span class="line"><span class="cl">│   ├── about.md / about.en.md   # About page
</span></span><span class="line"><span class="cl">│   ├── archives.md              # Archive
</span></span><span class="line"><span class="cl">│   └── search.md                # Search
</span></span><span class="line"><span class="cl">├── layouts/partials/
</span></span><span class="line"><span class="cl">│   ├── header.html              # Language switcher fix
</span></span><span class="line"><span class="cl">│   ├── comments.html            # Giscus comments
</span></span><span class="line"><span class="cl">│   ├── extend_head.html         # KaTeX + Schema
</span></span><span class="line"><span class="cl">│   └── math.html                # KaTeX scripts
</span></span><span class="line"><span class="cl">├── static/
</span></span><span class="line"><span class="cl">│   ├── favicon.ico              # Globe icon
</span></span><span class="line"><span class="cl">│   └── _headers                 # CF caching policy
</span></span><span class="line"><span class="cl">└── themes/PaperMod/             # git submodule
</span></span></code></pre></div><p>No bloat. The beauty of static sites is that the structure is transparent — no database, no server, no Docker. A YAML config, some Markdown files, a few HTML partials, and you have a complete website.</p>
<h2 id="looking-back">Looking Back</h2>
<p>The whole process took an afternoon. The most time-consuming parts weren&rsquo;t Hugo itself — Hugo&rsquo;s documentation is solid and the learning curve is gentle — but rather the &ldquo;should be simple but somehow isn&rsquo;t&rdquo; stuff: finding the Pages entry in Cloudflare&rsquo;s Dashboard, the language switcher jumping to homepage, picking the wrong Giscus category.</p>
<p>None of these were hard problems. But they all required actually hitting the wall, spending time debugging, and figuring out the fix. Writing it down here so I don&rsquo;t step on the same mines twice.</p>
<p>Blog&rsquo;s up. Time to write some real content.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
