<?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>Side Projects on blog.taikiito.com</title><link>https://blog.taikiito.com/en/tags/side-projects/</link><description>Recent content in Side Projects on blog.taikiito.com</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Sat, 22 Aug 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.taikiito.com/en/tags/side-projects/index.xml" rel="self" type="application/rss+xml"/><item><title>I Built This Blog a Hatena-Style Calendar, and Locked My Own JSON In a Box</title><link>https://blog.taikiito.com/en/posts/2026-08-22-sidebar-calendar/</link><pubDate>Sat, 22 Aug 2026 09:00:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-22-sidebar-calendar/</guid><description>A record of building a Hatena-Blog-style calendar, tag cloud, and monthly jump buttons into this blog&amp;rsquo;s own sidebar - including tracking down why the calendar silently failed to render, caused by Hugo&amp;rsquo;s html/template auto-escaping mangling an inline JSON blob.</description><content:encoded><![CDATA[<p>Last time, I wrote about shipping a Singlish glossary site alongside this blog. Back to the blog itself this time - a record of building its sidebar.</p>
<h2 id="the-post-list-and-archive-page-werent-quite-enough">The post list and archive page weren&rsquo;t quite enough</h2>
<p>This blog runs on Hugo + PaperMod. At first the header just had two menu items: &ldquo;Posts&rdquo; and &ldquo;Archive.&rdquo; As the post count grows, being able to browse by date or tag helps. PaperMod is a single-column theme though - there&rsquo;s no concept of a sidebar built in. The content column is fixed at a max width (<code>--nav-width: 1024px</code>) and centered, so on a wide screen there&rsquo;s blank space on either side. I figured something could go there.</p>
<h2 id="first-pass-just-two-buttons">First pass: just two buttons</h2>
<p>PaperMod ships an empty hook partial called <code>extend_footer.html</code> - override it and you can inject arbitrary HTML at the end of every page. There&rsquo;s also a convention where any CSS file dropped in <code>assets/css/extended/*.css</code> gets auto-concatenated into the theme&rsquo;s stylesheet. Using both, I added two fixed pill buttons in the right margin: &ldquo;📅 By date&rdquo; linking to <code>/archives/</code>, and &ldquo;🏷️ By tag&rdquo; linking to <code>/tags/</code>. Just shortcuts, nothing more.</p>
<h2 id="looking-at-hatena-blog-i-decided-to-rebuild-it">Looking at Hatena Blog, I decided to rebuild it</h2>
<p>Plain buttons felt thin. Hatena Blog&rsquo;s sidebar has an actual month-grid calendar with post days highlighted, and a tag cloud with names and counts sitting right there. I wanted that instead.</p>
<p>The tag cloud was easy - PaperMod&rsquo;s own <code>/tags/</code> page (<code>taxonomy.html</code>) already does exactly this, so I borrowed its logic directly:</p>
<pre tabindex="0"><code class="language-gotemplate" data-lang="gotemplate">{{ range $tagsPage.Data.Terms.Alphabetical }}
  {{ with site.GetPage (printf &#34;/tags/%s&#34; .Name) }}
    &lt;a href=&#34;{{ .Permalink }}&#34;&gt;{{ .LinkTitle }} ({{ $count }})&lt;/a&gt;
  {{ end }}
{{ end }}
</code></pre><p>The calendar took more thought since this is a static site generator. At build time, I embed each language&rsquo;s post dates, titles, and URLs as JSON inside the page (<code>{{ $calItems | jsonify }}</code>), then render the month grid with plain vanilla JS. No external library.</p>
<h2 id="the-calendar-rendered-nothing">The calendar rendered nothing</h2>
<p>I shipped it, and the tag cloud showed up fine - but the calendar area was just blank. The prev/next (‹ ›) buttons appeared; no day grid, no month title.</p>
<p>Checking the actual rendered HTML in devtools, I found this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;application/json&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;quick-cal-data&#34;</span><span class="p">&gt;</span><span class="s2">&#34;[{\&#34;date\&#34;:\&#34;2026-08-21\&#34;,\&#34;...\&#34;}]&#34;</span><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>The whole array had been escaped and wrapped in quotes <strong>as a string</strong>. Feed that into <code>JSON.parse()</code> and you get back a single string, not an array. The <code>.forEach()</code> right after it obviously doesn&rsquo;t exist on a string, so it threw and halted the rest of the script.</p>
<p>The cause was Hugo&rsquo;s template engine (<code>html/template</code>). When you interpolate a value inside a <code>&lt;script&gt;</code> tag, Hugo automatically treats it as untrusted and escapes it as a JS string literal. <code>jsonify</code> returns a plain <code>string</code>, so Hugo wrapped the whole thing in quotes rather than trusting it as raw JS.</p>
<p>The fix was one line - pipe it through <code>safeJS</code> to tell Hugo explicitly that this JS is safe to pass through as-is:</p>
<pre tabindex="0"><code class="language-gotemplate" data-lang="gotemplate">{{ $calItems | jsonify | safeJS }}
</code></pre><p>With that, the JSON landed as a real array again, and the calendar started working.</p>
<h2 id="adding-monthly-jump-buttons">Adding monthly jump buttons</h2>
<p>Once the calendar worked, I wanted a way to jump straight to a given month instead of stepping through prev/next one month at a time - which gets tedious as posts accumulate. I added a list of every year-month with at least one post, newest first; clicking one jumps the calendar there in place, no page reload, reusing the same state the prev/next buttons already manage.</p>
<h2 id="where-things-stand">Where things stand</h2>
<p>This post ends up being a record of the blog fixing itself while I was building it. Calendar, monthly buttons, and tag cloud are all in the sidebar now - next up is actually writing more posts.</p>
<p>To be continued.</p>
]]></content:encoded></item><item><title>Riding the Momentum of Starting the Blog, I Also Shipped a Singlish Glossary Site</title><link>https://blog.taikiito.com/en/posts/2026-08-22-singlish-lah/</link><pubDate>Sat, 22 Aug 2026 08:30:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-22-singlish-lah/</guid><description>A small side project shipped the same day as this blog: a plain HTML/CSS/JS Singlish-term glossary for Japanese speakers, plus the subdomain + repo pattern that later projects would reuse.</description><content:encoded><![CDATA[<p>Last post was about building the shell of this blog. Same day, riding the momentum, I shipped one more small site: a glossary of Singlish expressions you actually hear in daily conversation in Singapore, aimed at Japanese speakers.</p>
<h2 id="deliberately-a-different-stack">Deliberately a different stack</h2>
<p>The blog runs on Hugo, but this one is plain HTML/CSS/JS with no build step at all. If I&rsquo;m going to put projects out as a portfolio, showing range in approach felt better than reaching for the same stack every time. Data lives in a <code>terms.json</code> file, with search and category filtering built on top of it. At launch: 33 terms across 5 categories (stock phrases, reactions/emotions, food &amp; daily life, relationships/how people address each other, and things picked up at work).</p>
<p>Content is a mix of generally well-known Singlish terms plus a few I&rsquo;ve actually heard myself. The plan going forward is to keep appending to <code>terms.json</code> as new ones come up.</p>
<h2 id="the-subdomain-pattern-became-a-real-pattern">The subdomain pattern became a real pattern</h2>
<p>The domain-and-repo shape settled into an actual template at this point: one dedicated GitHub repo per project, one <code>&lt;project&gt;.taikiito.com</code> subdomain via a Cloudflare CNAME. One thing I learned here: the CNAME has to stay DNS-only (not proxied), or GitHub Pages&rsquo; automatic HTTPS certificate issuance gets delayed or fails outright.</p>
<h2 id="back-and-forth-on-the-footer-line">Back and forth on the footer line</h2>
<p>I rewrote the footer copy twice. The first version said something like &ldquo;hope this is useful to someone&rdquo; - writing it out, it felt a bit presumptuous, so I changed it to a more self-aware &ldquo;way too niche for anyone, but I made it for myself so here it is.&rdquo; Looking at that again, I decided neither was actually necessary and removed the line entirely. Stating what the thing is felt like enough; why it exists is something a reader can decide for themselves.</p>
<h2 id="two-more-things-shipped-same-day">Two more things shipped, same day</h2>
<p>The blog and this glossary, both shipped the same day, both riding the same subdomain pattern. Whatever comes next, that pattern makes it fast to stand up.</p>
<p>To be continued.</p>
]]></content:encoded></item><item><title>Before Writing a Single Post, I Renamed My GitHub Account</title><link>https://blog.taikiito.com/en/posts/2026-08-22-building-this-blog/</link><pubDate>Sat, 22 Aug 2026 08:00:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-22-building-this-blog/</guid><description>The decision to start publishing this blog: Hugo + PaperMod, GitHub Actions deploying to GitHub Pages under a custom domain, a private source repo force-pushing built output to a separate public repo, and a GitHub username rename along the way.</description><content:encoded><![CDATA[<p>Last post was about building an RSS reader and tearing it down the same day. Around the same time, I made a bigger call: to start writing down what I&rsquo;d been doing, in public. This blog is the result, and I started it that same day.</p>
<h2 id="why">Why</h2>
<p>The same newsletter that had introduced me to MulmoClaude in the first place also carried some advice from its author: if you want to move toward being an engineer, publish real work on GitHub. The domain setup, self-hosting, and email troubleshooting I&rsquo;d been doing seemed like the kind of thing that could be genuinely useful to someone hitting the same problem via search, so I decided to write it up.</p>
<h2 id="choosing-a-diary-format">Choosing a diary format</h2>
<p>I first considered topic-organized how-to posts, then switched partway through to a chronological diary - writing things in the order they actually happened, mistakes and detours included. It&rsquo;s closer to how it actually went.</p>
<h2 id="picking-the-stack">Picking the stack</h2>
<p>I went with Hugo as the static site generator, mainly because multi-language support (i18n) is built in. Theme: PaperMod. Deploy is GitHub Actions building the site and pushing it to GitHub Pages, with a custom domain (<code>blog.taikiito.com</code>) pointed at it via a Cloudflare CNAME.</p>
<p>I split it into two repos: the actual Hugo source I edit (<code>blog-source</code>) stays private, and a single Actions job builds it and force-pushes only the built static output to a separate public repo (<code>blog</code>). Editing history and drafts never leave the private repo - only the finished output is public, which I like.</p>
<h2 id="renamed-my-username-while-i-was-at-it">Renamed my username while I was at it</h2>
<p>I also renamed my GitHub username at this point. The old one had a string based on my birthdate in it, added without much thought when I first signed up, and I&rsquo;d wanted to change it for a while. My first choice was already taken, so I landed on a <code>-dev</code> suffix instead. With zero public repos at the time, the switching cost was basically nothing.</p>
<p>I also set up a GitHub profile README - creating a repo with the exact same name as your username makes it auto-display on your profile page. Kept it to two lines:</p>
<blockquote>
<p>Tinkering with computers on the side. Hoping it helps someone.</p>
</blockquote>
<h2 id="where-things-stand">Where things stand</h2>
<p>The blog itself and a public GitHub account are both in place now. The actual posts are what comes next, written up after the fact as things happen.</p>
<p>To be continued.</p>
]]></content:encoded></item><item><title>I Built Myself an RSS Reader, and Tore It Down the Same Day</title><link>https://blog.taikiito.com/en/posts/2026-08-21-rss-reader-teardown/</link><pubDate>Fri, 21 Aug 2026 10:00:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-21-rss-reader-teardown/</guid><description>Self-hosted a Miniflux RSS reader on the same server as MulmoClaude to aggregate news by category, then decided the same day it would just become an overflowing unread inbox, and removed the entire stack.</description><content:encoded><![CDATA[<p>Last post ended with the email sender-reputation issue - I&rsquo;d run out of technical levers to pull there and was just waiting it out. While waiting, I tried something else: a self-hosted RSS reader to keep up with news and blogs in one place.</p>
<h2 id="why-miniflux">Why Miniflux</h2>
<p>A few options were on the table, but I picked Miniflux for being lightweight and easy to self-host. The setup was one Docker Compose file - a Miniflux container plus a PostgreSQL container. Access followed the same pattern as MulmoClaude: a Cloudflare Access authentication gate in front.</p>
<p>The plan was to pull in the Straits Times, the NYT, and a handful of blogs, sorted by category. Twitter/X was out from the start - it dropped free RSS access back in 2023.</p>
<h2 id="what-subscribing-to-feeds-actually-revealed">What subscribing to feeds actually revealed</h2>
<p>Once I actually wired up the feeds, a few things weren&rsquo;t what I expected.</p>
<ul>
<li>The NHK feed had been silently blocked as of August 8th. No explanation given, and it&rsquo;s likely targeting cloud-provider IP ranges as a class, not anything specific to my setup.</li>
<li>The Straits Times and NYT feeds were both alive, but both stop at headline and summary before the paywall.</li>
</ul>
<p>Technically, I got it fully working.</p>
<h2 id="and-decided-to-stop-anyway">And decided to stop anyway</h2>
<p>Once it was working, I paused and thought about it. The idea of aggregating news by category wasn&rsquo;t bad on its own, but running it for real seemed like it would just become an ever-growing pile of unread items - an information-overload sink. The point had been to control how much I read, not to read more.</p>
<p>Same day, I removed the whole thing - the Docker containers, the nginx config, the DNS record, the Cloudflare Access application. Less than 24 hours between building it and tearing it down.</p>
<h2 id="getting-something-working-and-actually-keeping-it-are-different-things">Getting something working and actually keeping it are different things</h2>
<p>This one taught me that getting something technically working is a different question from whether it actually earns a permanent place in my routine. Deciding to build something and then walk away from it isn&rsquo;t necessarily a bad outcome.</p>
<p>To be continued.</p>
]]></content:encoded></item><item><title>With the Domain Working, I Built Myself a Digital Business Card the Same Day</title><link>https://blog.taikiito.com/en/posts/2026-08-19-digital-business-card/</link><pubDate>Wed, 19 Aug 2026 10:00:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-19-digital-business-card/</guid><description>A same-day side quest after getting the domain working: a static digital business card at a new subdomain, served entirely separately from the main app, with a vCard-download button, a WhatsApp click-to-chat link, and a small run-in with Cloudflare&amp;rsquo;s email obfuscation.</description><content:encoded><![CDATA[<p>Last time, I got <code>taikiito.com</code> working and could reach my own MulmoClaude from a browser. Now that I could carve out subdomains under it, I built one more thing the same day: a digital business card.</p>
<h2 id="one-subdomain-one-static-page">One subdomain, one static page</h2>
<p>I created a new subdomain, <code>card.taikiito.com</code>, and added a second nginx server block on the same Lightsail box. The important part: it&rsquo;s completely separate from the main Node.js app. No <code>proxy_pass</code> - just a single static HTML file being served - so traffic to the card page has zero effect on the main app&rsquo;s load.</p>
<p>The content is lifted straight from the business card I actually use day to day. I kept it down to two buttons:</p>
<ul>
<li>&ldquo;Save Contact&rdquo; - generates a <code>.vcf</code> file on the spot via a JS Blob and opens the phone&rsquo;s add-to-contacts sheet</li>
<li>&ldquo;WhatsApp&rdquo; - a <code>wa.me</code> click-to-chat deep link</li>
</ul>
<p>I originally had a third button for sending an email too, but later cut it down to just an info row with a <code>mailto:</code> link. Fewer buttons turned out to be more usable.</p>
<h2 id="a-small-color-detour">A small color detour</h2>
<p>I&rsquo;d built it in navy from the start, but made a black-and-white version too, just to compare side by side. Navy won, and the black-and-white draft got deleted.</p>
<h2 id="a-small-gotcha">A small gotcha</h2>
<p>Right after launch, previewing the card inside this app&rsquo;s own embedded iframe, the email address showed up as a garbled placeholder string instead of the real thing. My first thought was I&rsquo;d broken something. Turned out to be Cloudflare&rsquo;s &ldquo;Email Address Obfuscation&rdquo; (Scrape Shield) - it automatically encodes email addresses on the page to deter spam scrapers, and a small decoding script is supposed to restore the real address in a normal browser. That decoder just doesn&rsquo;t run inside this app&rsquo;s preview iframe. It&rsquo;s a zone-wide setting, so I left it on and confirmed the real address renders fine in an actual browser tab.</p>
<h2 id="what-a-name-domain-unlocks">What a name-domain unlocks</h2>
<p>The moment my own name became a domain, spinning up small subdomains like this got trivial. A few more of the same pattern would follow.</p>
<p>To be continued.</p>
]]></content:encoded></item></channel></rss>