<?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>Algolia on My Little Blog</title><link>https://www.parkerchenca.com/tags/algolia/</link><description>Recent content in Algolia on My Little Blog</description><generator>Hugo</generator><language>en-us</language><copyright>Parker Chen</copyright><lastBuildDate>Wed, 09 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.parkerchenca.com/tags/algolia/index.xml" rel="self" type="application/rss+xml"/><item><title>Building This Blog</title><link>https://www.parkerchenca.com/tech/building-this-blog/</link><pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.parkerchenca.com/tech/building-this-blog/</guid><description>Hugo, PaperMod, Algolia, and the four things that broke along the way.</description><content:encoded><![CDATA[<p>This site runs on Hugo with the PaperMod theme, bilingual content, and Algolia
for search. None of those choices were difficult. Wiring them together produced
four bugs that were, so this post is mostly about those.</p>
<h2 id="why-hugo">Why Hugo</h2>
<p>The decision came down to Hugo against Hexo. Two requirements settled it: content
organised by directory, and a tolerable path for Org-mode files.</p>
<p>Hugo maps <code>content/</code> directly onto URL structure. A file at
<code>content/tech/foo.md</code> becomes <code>/tech/foo/</code> and the <code>tech</code> directory becomes a
section with its own list page. No plugin, no configuration. Hexo can be pushed
into the same shape with <code>permalink</code> rules and a category plugin, but it is a
workaround rather than the design.</p>
<h2 id="the-org-mode-detour">The Org-mode detour</h2>
<p>Hugo ships Org support through go-org, so <code>.org</code> files render without a plugin.
That worked, briefly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">ERROR access denied: &#34;text/org&#34; is not whitelisted in policy &#34;security.allowContent&#34;
</span></span></code></pre></div><p>Hugo 0.166 denies <code>text/org</code> by default. The parser is present; the security
policy blocks it. Adding an override fixed it:</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">security</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">allowContent</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s1">&#39;! ^text/html$&#39;</span><span class="w">
</span></span></span></code></pre></div><p>The override came back out later. Writing Org and exporting to Markdown with
ox-hugo keeps a single renderer in play, so Hugo only ever sees Markdown and the
security policy stays at its default. One less deviation from upstream.</p>
<h2 id="configuration-renames">Configuration renames</h2>
<p>Two config keys changed name in Hugo 0.158 and the older names still appear in
most tutorials:</p>
<table>
	<thead>
			<tr>
					<th>Old</th>
					<th>New</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>languageCode</code></td>
					<td><code>locale</code></td>
			</tr>
			<tr>
					<td><code>languages.&lt;lang&gt;.languageName</code></td>
					<td><code>languages.&lt;lang&gt;.label</code></td>
			</tr>
	</tbody>
</table>
<p>Both still work and both emit deprecation warnings. PaperMod&rsquo;s templates still
call <code>.Language.LanguageCode</code> internally, so the warnings appear even with a
fully modern config. They are the theme&rsquo;s, not yours.</p>
<h2 id="search">Search</h2>
<p>PaperMod bundles Fuse.js. It builds a JSON index at compile time, ships it to
the browser, and matches locally. Zero infrastructure, and for a small English
site it is the correct choice.</p>
<p>Two things pushed this site to Algolia instead. The whole index downloads on
every visit to the search page, which does not scale past a few hundred posts.
And Fuse does character-level fuzzy matching with no CJK tokenisation, which
matters for a site that is half Chinese.</p>
<p>There is no official Algolia integration for Hugo. The community packages —
<code>hugo-algolia</code>, <code>atomic-algolia</code> — stopped being maintained years ago and are
pinned to client v4 while current is v5. So: a template that emits records, a
search page built on InstantSearch, and a push script. Under 200 lines total,
all of it sitting on official libraries.</p>
<h3 id="the-escaping-bug">The escaping bug</h3>
<p>The search page passes configuration into JavaScript:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">cfg</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">{{</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;appId&#34;</span><span class="w"> </span><span class="err">$</span><span class="nx">appId</span><span class="w"> </span><span class="s">&#34;indexName&#34;</span><span class="w"> </span><span class="err">$</span><span class="nx">index</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">jsonify</span><span class="w"> </span><span class="p">}};</span><span class="w">
</span></span></span></code></pre></div><p>Go&rsquo;s <code>html/template</code> knows this is a JavaScript context and escapes the value
into a <em>string literal</em>. The result:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">cfg</span> <span class="o">=</span> <span class="s2">&#34;{\&#34;appId\&#34;:\&#34;...\&#34;,\&#34;indexName\&#34;:\&#34;blog_en\&#34;}&#34;</span><span class="p">;</span>
</span></span></code></pre></div><p><code>cfg.indexName</code> is <code>undefined</code>. InstantSearch initialises against an undefined
index and the page renders an empty search box with no error. Wrapping the value
in <code>JSON.parse()</code> fixes it without reaching for <code>safeJS</code>, which would have
disabled the escaping that exists for good reason.</p>
<h3 id="the-truncate-bug">The truncate bug</h3>
<p>Records are truncated to stay under Algolia&rsquo;s 10KB limit:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">.</span><span class="nx">Content</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">plainify</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">htmlUnescape</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">truncate</span><span class="w"> </span><span class="mi">2000</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Entities kept appearing in the index as <code>&amp;#34;</code>. Hugo&rsquo;s <code>truncate</code> is HTML-aware
and re-escapes on output, so it has to run <em>before</em> the unescaping, not after.</p>
<h3 id="the-leftover-fuse-bug">The leftover Fuse bug</h3>
<p>PaperMod injects its Fuse bundle conditionally:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">eq</span><span class="w"> </span><span class="p">.</span><span class="nx">Layout</span><span class="w"> </span><span class="s">`search`</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nx">link</span><span class="w"> </span><span class="nx">rel</span><span class="p">=</span><span class="s">&#34;preload&#34;</span><span class="w"> </span><span class="nx">as</span><span class="p">=</span><span class="s">&#34;fetch&#34;</span><span class="w"> </span><span class="nx">href</span><span class="p">=</span><span class="s">&#34;../index.json&#34;</span><span class="p">&gt;</span><span class="w">
</span></span></span></code></pre></div><p>The Algolia search page still declared <code>layout: &quot;search&quot;</code>, so the theme injected
15KB of Fuse and preloaded an <code>index.json</code> that no longer existed. Renaming the
layout to <code>algolia</code> sidesteps the condition entirely.</p>
<h2 id="what-is-left">What is left</h2>
<p>Comments are wired to giscus but switched off — the partial renders nothing
until a repository is configured. Analytics is a commented-out block waiting on
a GA4 property. Both are switches rather than commitments.</p>
<p>The remaining duplication is the Algolia index name, which appears in
<code>hugo.yaml</code> and again in the push script. Removing it would mean teaching the
script to parse YAML. Two constants do not justify a dependency.</p>
<h2 id="notes-to-self">Notes to self</h2>
<ul>
<li>Run <code>hugo server -e production</code> when checking SEO tags; the default
development environment omits OpenGraph, Twitter Cards, and JSON-LD entirely.</li>
<li>The index has to be pushed after every deploy. Fuse indexes travel with the
build; Algolia indexes live on someone else&rsquo;s server.</li>
<li>Test content needs to be long enough to scroll. The back-to-top button only
appears after a full viewport of scrolling, which a sixty-word post will never
trigger.</li>
</ul>
]]></content:encoded></item></channel></rss>