Repos / hi.imnhan.com / ed908f845c
commit ed908f845cc5ae48c9229cfc9c2e0f92f40c487f
Author: Nhân <hi@imnhan.com>
Date:   Thu Aug 31 19:31:25 2023 +0700

    heading autolink

diff --git a/_s4g/theme/base.css b/_s4g/theme/base.css
index 1d5a594..52f77f8 100644
--- a/_s4g/theme/base.css
+++ b/_s4g/theme/base.css
@@ -38,6 +38,24 @@ a[target="_blank"]::after {
   content: "↗";
 }
 
+a.heading-link {
+  text-decoration: none;
+  margin-left: 0.3em;
+  visibility: hidden;
+}
+:hover > a.heading-link {
+  visibility: visible;
+}
+section:target > h2,
+section:target > h3,
+section:target > h4,
+section:target > h5,
+section:target > h6 {
+  background-color: #beefff;
+  margin-left: -1rem;
+  padding-left: 1rem;
+}
+
 main img,
 main video {
   max-width: 100%;
diff --git a/chromebook/index.html b/chromebook/index.html
index 95bca27..9816021 100644
--- a/chromebook/index.html
+++ b/chromebook/index.html
@@ -51,7 +51,7 @@ <h1>Acer Chromebook Spin 713 &#34;Voxel&#34;: an adequate Crostini device, a bug
 price at <a href="https://www.acer.com/us-en/chromebooks/acer-chromebook-enterprise-spin-713-cp713-3w/pdp/NX.AHAAA.006">$1,099.99</a> (lol). Maybe consider buying if you can find it at a
 heavy discount and the speakers issue has been fixed somehow.</p>
 <section id="Context">
-<h2>Context</h2>
+<h2>Context<a href="#Context" class="heading-link">#</a></h2>
 <p>Around 2022 I was looking for a replacement for my T530—something lighter
 with a better screen—and saw a listing for a used Acer Chromebook Spin 713-3W
 at only 10mil VND ($425, give or take). A recently released HiDPI laptop with
@@ -61,7 +61,7 @@ <h2>Context</h2>
 based development <a href="https://git.sr.ht/~nhanb/neodots">environment</a> on Crostini.</p>
 </section>
 <section id="The-hardware">
-<h2>The hardware</h2>
+<h2>The hardware<a href="#The-hardware" class="heading-link">#</a></h2>
 <ul>
 <li>
 Specs: i5-1135G7 (Tiger Lake), Xe graphics, 8GB RAM, 256GB SSD
@@ -82,7 +82,7 @@ <h2>The hardware</h2>
 <p><img alt="thickness" src="voxel_thickness.jpg"></p>
 </section>
 <section id="ChromeOS-Crostini">
-<h2>ChromeOS/Crostini</h2>
+<h2>ChromeOS/Crostini<a href="#ChromeOS-Crostini" class="heading-link">#</a></h2>
 <blockquote>
 <p>I’d just like to interject for a moment. What you’re referring to as Linux,
 is in fact, CHROME/Linux, or as I’ve recently taken to calling it, CHROME
@@ -126,7 +126,7 @@ <h2>ChromeOS/Crostini</h2>
 <p>So I jumped ship.</p>
 </section>
 <section id="Real-Arch-Linux-on-MrChromebox-UEFI">
-<h2>Real Arch Linux on MrChromebox UEFI</h2>
+<h2>Real Arch Linux on MrChromebox UEFI<a href="#Real-Arch-Linux-on-MrChromebox-UEFI" class="heading-link">#</a></h2>
 <p>Installing <em>real</em> Linux on this device requires installing the MrChromebox
 custom UEFI firmware, which in turn requires disabling the firmware write
 protection. Fortunately, for this device all I needed to do was opening up the
@@ -168,7 +168,7 @@ <h2>Real Arch Linux on MrChromebox UEFI</h2>
 <p>But hey, I would have never known any of this if I hadn’t tried, right?</p>
 </section>
 <section id="Aside-Chromebook-keyboard-quirks-on-KDE">
-<h2>Aside: Chromebook keyboard quirks on KDE</h2>
+<h2>Aside: Chromebook keyboard quirks on KDE<a href="#Aside-Chromebook-keyboard-quirks-on-KDE" class="heading-link">#</a></h2>
 <p>The most glaring issue is the absence of the <code>windows</code> key (aka <code>super</code>,
 <code>hyper</code>, or <code>meta</code>). It’s not a huge problem for me: I always make <code>capslock</code>
 act as a <code>ctrl</code> key, so I can turn the original <code>ctrl</code> into <code>windows</code> instead:</p>
diff --git a/cool/index.html b/cool/index.html
index fbc6d11..957f01e 100644
--- a/cool/index.html
+++ b/cool/index.html
@@ -60,23 +60,23 @@ <h1>&#34;Have you built anything cool?&#34;</h1>
 <p>If you—Nicholas—are reading this and don’t want all the nerdy stuff, here are my condensed
 answers:</p>
 <section id="Have-you-built-anything">
-<h3>Have you built anything?</h3>
+<h3>Have you built anything?<a href="#Have-you-built-anything" class="heading-link">#</a></h3>
 <p>Yes, I have made desktop and Android games, a movie ticket sales program, a desktop manga
 grabber, a web version of it that talks to dropbox, and several small shell scripts / web
 utilities.</p>
 </section>
 <section id="So-nothing-cool">
-<h3>So, nothing cool?</h3>
+<h3>So, nothing cool?<a href="#So-nothing-cool" class="heading-link">#</a></h3>
 <p>If you’re neither a tech geek nor an otaku (which I assume you’re not) then no, there’s probably
 nothing I’ve done that you would find interesting.</p>
 </section>
 <section id="Nerd-alert">
-<h2>Nerd alert!</h2>
+<h2>Nerd alert!<a href="#Nerd-alert" class="heading-link">#</a></h2>
 <p>The rest of this post is aimed at the <del>nerdier</del> more tech-savvy audience. You have been
 warned ;)</p>
 </section>
 <section id="First-year-Welcome-to-the-web-and-the-GUI-programming-disillusionment">
-<h2>First year: Welcome to the web, and the GUI programming disillusionment</h2>
+<h2>First year: Welcome to the web, and the GUI programming disillusionment<a href="#First-year-Welcome-to-the-web-and-the-GUI-programming-disillusionment" class="heading-link">#</a></h2>
 <p>I had touched <em>web stuff</em> before in high school: a vBulletin forum that I created (unofficially)
 for students in my middle school. However, I only properly learned PHP and JS when I started the
 Web Programming course here. With (moderately) great power came great desires, so I set out to
@@ -113,13 +113,13 @@ <h2>First year: Welcome to the web, and the GUI programming disillusionment</h2>
 desktop Linux user for that matter).</p>
 </section>
 <section id="Second-year-enough-of-this-bull-school-crap-I-m-making-stuff-for-myself">
-<h2>Second year: enough of this <del>bull</del> school crap. I’m making stuff for myself!</h2>
+<h2>Second year: enough of this <del>bull</del> school crap. I’m making stuff for myself!<a href="#Second-year-enough-of-this-bull-school-crap-I-m-making-stuff-for-myself" class="heading-link">#</a></h2>
 <p>To be fair, the following year has offered a number of new stuff: C/C++ programming, a taste of the
 M$ .NET C# stack (still impressed by Visual Studio’s vi mode plugin), more Java,
 <a href="http://truongtx.me/2013/05/02/agent-069-game/">Android app programming</a>, and some neat security
 tricks. However, none of those intrigued me much, so I decided to start making things for my own:</p>
 <section id="Shell-scripts">
-<h3>Shell scripts</h3>
+<h3>Shell scripts<a href="#Shell-scripts" class="heading-link">#</a></h3>
 <p>If you have taken a look at my <a href="https://github.com/nhanb/dotfiles">dotfiles</a>, you’ll notice that I
 do write a bunch of shell scripts to automate stuff I do often. The one I’m currently proudest of
 is <a href="https://github.com/nhanb/dotfiles/blob/master/scripts/rmitproxy_silent">rmiproxy_silent</a>, a
@@ -132,7 +132,7 @@ <h3>Shell scripts</h3>
 since sliced bread! Those guys are awesome!</p>
 </section>
 <section id="AJMG-then-Pytaku-then-who-knows">
-<h3>AJMG, then Pytaku, then… who knows?</h3>
+<h3>AJMG, then Pytaku, then… who knows?<a href="#AJMG-then-Pytaku-then-who-knows" class="heading-link">#</a></h3>
 <p>The original idea was actually creating a Java Swing program that helps download manga. It was born
 out of frustration of <a href="http://blog.domdomsoft.com/">DomDomSoft</a>, a manga downloader that requires
 “donation” to unlock full functionalities. “I could do that, and I’ll open source the crap out of
@@ -163,7 +163,7 @@ <h3>AJMG, then Pytaku, then… who knows?</h3>
 </section>
 </section>
 <section id="To-sum-it-up">
-<h2>To sum it up…</h2>
+<h2>To sum it up…<a href="#To-sum-it-up" class="heading-link">#</a></h2>
 <p>I don’t believe in developing products I myself don’t want to use. I want to make things that make
 my life easier, and if that helps others too then it’s a huge bonus. This is why Github is my
 favorite company right now, and
diff --git a/custom-theme/index.html b/custom-theme/index.html
index 6673a38..e7f4de4 100644
--- a/custom-theme/index.html
+++ b/custom-theme/index.html
@@ -68,7 +68,7 @@ <h1>Look ma, no stock theme!</h1>
 <p>I’m not a professional designer, but everything turned out quite well if I could say so. In this
 post I’ll explain my design goals and how I (hopefully) achieved them.</p>
 <section id="Clean-and-lightweight">
-<h2>Clean and lightweight</h2>
+<h2>Clean and lightweight<a href="#Clean-and-lightweight" class="heading-link">#</a></h2>
 <p>The web is messy. I’m not talking blinking-marquee-fire-animated-header messy (thank god we’re done
 with that… <a href="https://developers.google.com/fonts/docs/getting_started#Effects">or are we?</a>). I’m talking megabytes-of-useless-javascript-and-css messy. For
 whatever reason, some people now think it’s cool to include Bootstrap/Foundation to every project,
@@ -84,12 +84,12 @@ <h2>Clean and lightweight</h2>
 Vietnamese posts are rendered in a font that supports it. Oh well, at least it looks awesome.</p>
 </section>
 <section id="Content-is-king">
-<h2>Content is king</h2>
+<h2>Content is king<a href="#Content-is-king" class="heading-link">#</a></h2>
 <p>No more distracting sidebar with “latest news”, “related posts”, etc. with thumbnails popping up
 all over the place, just a good old article body from start to finish.</p>
 </section>
 <section id="Easy-on-the-eye">
-<h2>Easy on the eye</h2>
+<h2>Easy on the eye<a href="#Easy-on-the-eye" class="heading-link">#</a></h2>
 <p>While <a href="http://bettermotherfuckingwebsite.com/">bettermotherfuckingwebsite</a> does a good job at demonstrating how far you can go with a
 few simple CSS rules (hint: very far), I found Tommi Kaikkonen’s <a href="http://www.kaikkonendesign.fi/typography/">Interactive Guide to Blog
 Typography</a> much more comprehensive and informative. I implemented many of the ideas found
@@ -111,7 +111,7 @@ <h2>Easy on the eye</h2>
 </ul>
 </section>
 <section id="That-s-it">
-<h2>That’s it!</h2>
+<h2>That’s it!<a href="#That-s-it" class="heading-link">#</a></h2>
 <p>You can find the source code to my theme <a href="https://github.com/nhanb/motherfucking-pelican-theme">on GitHub</a>. I don’t recommend using it as-is though,
 since I haven’t implemented many required templates (authors, tags, categories, etc.) because I
 don’t use them. There’s also the hardcoded content in footer and probably a few more places. Maybe
diff --git a/fcitx/index.html b/fcitx/index.html
index c17beda..00ed61f 100644
--- a/fcitx/index.html
+++ b/fcitx/index.html
@@ -61,7 +61,7 @@ <h1>Dẹp ibus-unikey đi, dùng fcitx-unikey nhé!</h1>
 Trung Ngô—một trong những người phát triển chính của <a href="http://ibus-bogo.readthedocs.org/">ibus-bogo</a>—đã viết một bài blog rất hay về tình
 trạng gõ tiếng Việt hiện nay trên linux nói chung, ai quan tâm có thể tham khảo thêm <a href="https://lewtds.notion.site/c-m-b-g-ki-u-Unikey-tr-n-Linux-9b12cc2fcdbe43149b10eefc7db6b161">ở đây</a>.</p>
 <section id="fcitx-unikey">
-<h2>fcitx-unikey</h2>
+<h2>fcitx-unikey<a href="#fcitx-unikey" class="heading-link">#</a></h2>
 <p>Lọ mọ trên trang github của bogo, mình vô tình phát hiện ra <a href="https://github.com/BoGoEngine/fcitx-bogo">fcitx-bogo</a>: dự án này thực chất
 cũng dùng bogo-engine nhưng chạy với <a href="https://github.com/fcitx/fcitx">fcitx</a> chứ không phải ibus như bình thường. Rất tiếc là
 khi mình cài đặt và chạy thử fcitx-bogo thì nó luôn crash fcitx trước khi xử lý ra được chữ tiếng
@@ -88,7 +88,7 @@ <h2>fcitx-unikey</h2>
 <p><img alt="" src="fcitx-skype.png"></p>
 </section>
 <section id="vim-fcitx">
-<h2>vim-fcitx</h2>
+<h2>vim-fcitx<a href="#vim-fcitx" class="heading-link">#</a></h2>
 <p>Những ai đã thử gõ tiếng Việt trên vim chắc chắn đều biết: không tài nào dùng normal mode khi
 preedit đang bật được. Ngày xưa khi dùng ibus mình có thử viết <a href="https://github.com/nhanb/vim-bogo">một plugin</a> để bật tiếng Việt
 khi vào insert mode và trở lại tiếng Anh khi ra normal mode, nhưng cuối cùng không dùng vì preedit
diff --git a/fightstick-1/index.html b/fightstick-1/index.html
index fa7e48c..bd8f549 100644
--- a/fightstick-1/index.html
+++ b/fightstick-1/index.html
@@ -48,7 +48,7 @@ <h1>My first DIY fightstick: Part 1</h1>
 design</a> on Slagcoin:</p>
 <p><img alt="" src="http://www.slagcoin.com/joystick/example2/simple1.jpg"></p>
 <section id="Materials-and-tools">
-<h2>Materials and tools</h2>
+<h2>Materials and tools<a href="#Materials-and-tools" class="heading-link">#</a></h2>
 <p>If you live in Hồ Chí Minh City like me, you can probably get everything you need from shops on
 Bạch Đằng Street. The only hard part for me was convincing a small wood shop to sell MDF panels cut
 to my specifications in small quantity:</p>
@@ -102,7 +102,7 @@ <h2>Materials and tools</h2>
 build this way.</p>
 </section>
 <section id="Actually-making-it">
-<h2>Actually making it</h2>
+<h2>Actually making it<a href="#Actually-making-it" class="heading-link">#</a></h2>
 <p>I drilled holes in the front side: first using the 30mm bit to drill halfway, then went all the way
 with the 24mm bit. This allows a small button to be placed deep into the case, avoiding
 accidentally button presses.</p>
@@ -201,7 +201,7 @@ <h2>Actually making it</h2>
 <p><img alt="" src="fightstick_24_gap.jpg"></p>
 </section>
 <section id="Um-that-s-it-for-now">
-<h2>Um… that’s it (for now).</h2>
+<h2>Um… that’s it (for now).<a href="#Um-that-s-it-for-now" class="heading-link">#</a></h2>
 <p>That’s all of my progress so far. I’m visiting my <a href="http://fablabsaigon.org/">local Fablab</a>, which is advertised to
 have CNC machines and laser cutters so I’ll probably have a chance to produce more accurate panels.
 Hell, I may even redo the whole thing using machine-cut pieces for absolute accuracy and save
diff --git a/fightstick-2/index.html b/fightstick-2/index.html
index 47b2cf0..9f94659 100644
--- a/fightstick-2/index.html
+++ b/fightstick-2/index.html
@@ -44,7 +44,7 @@ <h1>My first DIY fightstick: Part 2</h1>
 the whole thing for a while, but yesterday I needed a distraction so… everything came together
 much more nicely than I thought!</p>
 <section id="I-did-it">
-<h2>I did it!</h2>
+<h2>I did it!<a href="#I-did-it" class="heading-link">#</a></h2>
 <p>I was almost done in Part 1, except for the fact that button holes on the top panels didn’t align,
 and the topmost panel flexed a bit because of imprecise screw holes:</p>
 <p><img alt="Ewww" src="../fightstick-1/fightstick_24_gap.jpg"></p>
@@ -78,7 +78,7 @@ <h2>I did it!</h2>
 <p><img alt="" src="fightstick2_03_front.jpg"></p>
 </section>
 <section id="Thoughts">
-<h2>Thoughts</h2>
+<h2>Thoughts<a href="#Thoughts" class="heading-link">#</a></h2>
 <p>All in all, this turned out much better than I expected. The whole thing feels solid to play with,
 and the plexiglass top panel makes a nice feeling palm rest. However, since I went a bit liberal on
 panel layers, it’s a tad heavier than my previous stick (which is already heavier than your average
diff --git a/go-stack/index.html b/go-stack/index.html
index b7ea63a..35749b3 100644
--- a/go-stack/index.html
+++ b/go-stack/index.html
@@ -47,7 +47,7 @@ <h1>Go, Postgres, Caddy, systemd: a simple, highly portable, Docker-free web sta
 backed by a Postgres database, fronted by Caddy which does TLS termination &amp;
 automatic Let’s Encrypt cert renewal, supervised &amp; isolated by systemd.</p>
 <section id="Go">
-<h2>Go</h2>
+<h2>Go<a href="#Go" class="heading-link">#</a></h2>
 <p>If you’re new to Go like me, you may find it helpful to skim the book <a href="https://lets-go.alexedwards.net/">Let’s
 Go</a> by Alex Edwards. It demonstrates helpful patterns so you can quickly put
 together a web service with little more than the Go standard library. However,
@@ -105,7 +105,7 @@ <h2>Go</h2>
 </ul>
 </section>
 <section id="Postgres">
-<h2>Postgres</h2>
+<h2>Postgres<a href="#Postgres" class="heading-link">#</a></h2>
 <p>While SQLite is a fine choice for small-to-medium sites, it does have its own
 quirks: so-called <a href="https://www.sqlite.org/flextypegood.html">flexible type checking</a> and <a href="https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes">limited ALTER TABLE
 capabilities</a> are my two pet peeves.</p>
@@ -171,7 +171,7 @@ <h2>Postgres</h2>
 anyway. This brings us to…</p>
 </section>
 <section id="systemd">
-<h2>systemd</h2>
+<h2>systemd<a href="#systemd" class="heading-link">#</a></h2>
 <p>Inevitably you’ll need something to manage your service process: autostart on
 boot, report/restart when it goes down, piping logs to the right place, that
 sort of thing. People used to install things like <a href="http://supervisord.org/">supervisord</a> for that.
@@ -213,7 +213,7 @@ <h2>systemd</h2>
 </code></pre>
 </section>
 <section id="Caddy">
-<h2>Caddy</h2>
+<h2>Caddy<a href="#Caddy" class="heading-link">#</a></h2>
 <p>Nowadays I prefer <a href="https://caddyserver.com/">Caddy</a> as the TLS-terminating reverse proxy instead of
 nginx, since it transparently performs Let’s Encrypt’s ACME challenge behind
 the scene. With my web service listening at localhost:8000, it literally takes
@@ -238,7 +238,7 @@ <h2>Caddy</h2>
 fiddling with certbot cron jobs.</p>
 </section>
 <section id="Closing-remarks">
-<h2>Closing remarks</h2>
+<h2>Closing remarks<a href="#Closing-remarks" class="heading-link">#</a></h2>
 <p>For a proper production-grade service, there’s more to be done:
 personally I’m using <code>ufw</code> to lock down everything except for the HTTP(S) ports
 and wireguard (I’m doing ssh over wireguard only too). Enabling unattended
diff --git a/hdviet/index.html b/hdviet/index.html
index ea44de9..3da7fa4 100644
--- a/hdviet/index.html
+++ b/hdviet/index.html
@@ -49,7 +49,7 @@ <h1>How I bypassed my university&#39;s domain blocker to watch movies on hdviet.
 do the trick way better and without any hassle. If you’re using Linux or you simply want to learn
 more about this stuff, read on!</p>
 <section id="The-problem">
-<h2>The problem</h2>
+<h2>The problem<a href="#The-problem" class="heading-link">#</a></h2>
 <p>This semester the RMIT-WPA wifi network no longer requires manual proxy configuration (probably
 because it makes Web Programming students miserable - they have to use Google App Engine), which is
 good news. Nevertheless, that annoying domain filter is still up and running, meaning we still
@@ -62,7 +62,7 @@ <h2>The problem</h2>
 <p><img alt="" src="hdviet_02_forbidden_direct.png"></p>
 </section>
 <section id="Going-for-the-IP">
-<h2>Going for the IP</h2>
+<h2>Going for the IP<a href="#Going-for-the-IP" class="heading-link">#</a></h2>
 <p>Naturally, I wanted to check if I could access the resource directly via the IP. An easy way to look
 up a domain’s IP is using <a href="http://ping.eu/ping/">ping.eu</a>. Once you’ve got the IP, try replacing the domain with it in
 the failed request:</p>
@@ -73,7 +73,7 @@ <h2>Going for the IP</h2>
 automatically replace <code>v-01.vn-hd.com</code> with the IP in all of the requests.</p>
 </section>
 <section id="Twisted-proxy">
-<h2>Twisted proxy</h2>
+<h2>Twisted proxy<a href="#Twisted-proxy" class="heading-link">#</a></h2>
 <p>Since changing the request destination directly in the browser is probably difficult (I don’t think
 Google Chrome even allows that), we’ll use an HTTP(S) proxy. This is when Twisted comes in handy.</p>
 <p><a href="https://twistedmatrix.com/">Twisted</a> is a battery-included framework to build robust network applications. By
@@ -98,7 +98,7 @@ <h2>Twisted proxy</h2>
 that.</p>
 </section>
 <section id="Domain-to-IP">
-<h2>Domain to IP</h2>
+<h2>Domain to IP<a href="#Domain-to-IP" class="heading-link">#</a></h2>
 <p>Open <code>server.py</code>, look for this part:</p>
 <pre><code class="language-python">class ConnectProxyRequest(ProxyRequest):
     """HTTP ProxyRequest handler (factory) that supports CONNECT"""
@@ -173,7 +173,7 @@ <h2>Domain to IP</h2>
 This leads to our final trick:</p>
 </section>
 <section id="Google-App-Engine-to-the-rescue">
-<h2>Google App Engine to the rescue!</h2>
+<h2>Google App Engine to the rescue!<a href="#Google-App-Engine-to-the-rescue" class="heading-link">#</a></h2>
 <p>Because a subtitle file is just plain text, its size is negligible. We can set up an external
 website that receives our original request, fetches the requested file on hdviet’s server and
 returns the requested file’s content back to us. I have already set up a proof-of-concept Google
diff --git a/ideas.html b/ideas.html
index bc9bb72..5cbd6c3 100644
--- a/ideas.html
+++ b/ideas.html
@@ -35,7 +35,7 @@
 <h1>Potential project ideas</h1>
 
 <section id="Self-hosted-RSS-reader-in-D">
-<h2>Self hosted RSS reader in D</h2>
+<h2>Self hosted RSS reader in D<a href="#Self-hosted-RSS-reader-in-D" class="heading-link">#</a></h2>
 <ul>
 <li>
 Single executable, web based
@@ -55,7 +55,7 @@ <h2>Self hosted RSS reader in D</h2>
 </ul>
 </section>
 <section id="Desktop-GUI-blogging-CMS-using-tkinter">
-<h2>Desktop GUI blogging CMS using tkinter</h2>
+<h2>Desktop GUI blogging CMS using tkinter<a href="#Desktop-GUI-blogging-CMS-using-tkinter" class="heading-link">#</a></h2>
 <ul>
 <li>
 Native win/mac look-and-feel, acceptable on linux (<code>clam</code> looks alright)
@@ -83,7 +83,7 @@ <h2>Desktop GUI blogging CMS using tkinter</h2>
 </ul>
 </section>
 <section id="Discord-bot-that-launches-CSGO-etc-server-on-demand">
-<h2>Discord bot that launches CSGO/etc. server on demand</h2>
+<h2>Discord bot that launches CSGO/etc. server on demand<a href="#Discord-bot-that-launches-CSGO-etc-server-on-demand" class="heading-link">#</a></h2>
 <p>Eyeing Linode’s 4GB RAM tier at $0.03/hr. But in general it should work on any
 cloud VPS that supports (almost) instant VM launch via API, and snapshots.</p>
 <ul>
diff --git a/linux-automation/index.html b/linux-automation/index.html
index ae45b5d..d289674 100644
--- a/linux-automation/index.html
+++ b/linux-automation/index.html
@@ -44,7 +44,7 @@ <h1>Why I use Linux: Automation</h1>
 will keep the nitpickers away.)</p>
 <p>First let’s discuss <em>why</em> automation rocks.</p>
 <section id="Repetition-is-evil-and-boring">
-<h2>Repetition is evil (and boring)</h2>
+<h2>Repetition is evil (and boring)<a href="#Repetition-is-evil-and-boring" class="heading-link">#</a></h2>
 <p>As a (would-be) software engineer, the <em>repetition is evil</em> notion has been planted in my head for
 far more times than anything else, and for good reasons.</p>
 <p>People are far more prone to error than computers, and doing repetitive tasks creates just too
@@ -58,7 +58,7 @@ <h2>Repetition is evil (and boring)</h2>
 generation).</p>
 </section>
 <section id="Automation-needs-command-line-tools">
-<h2>Automation needs command line tools</h2>
+<h2>Automation needs command line tools<a href="#Automation-needs-command-line-tools" class="heading-link">#</a></h2>
 <p>Because, of course, GUI programs are (nearly) impossible to interact with in our scripts. Sure
 you can try mouse click emulation tools and stuff like that, but is it really worth the effort?
 And I’d bet anything that those tools are far from reliable (GUI latency, anyone?).</p>
@@ -68,7 +68,7 @@ <h2>Automation needs command line tools</h2>
 aria2… Almost anything you can think of is available as a command line tool.</p>
 </section>
 <section id="Putting-them-all-together">
-<h2>Putting them all together</h2>
+<h2>Putting them all together<a href="#Putting-them-all-together" class="heading-link">#</a></h2>
 <p>Just like any UNIX-like system, Linux tools utilize the One True Phylosophy: Do only 1 thing, and
 do it well. (okay, I’m paraphrasing a bit, but you get the idea)</p>
 <p>The true power of command line tools is when they are used together. Let’s take a look at a
diff --git a/manjaro-xfce/index.html b/manjaro-xfce/index.html
index 4111468..979ee5a 100644
--- a/manjaro-xfce/index.html
+++ b/manjaro-xfce/index.html
@@ -50,7 +50,7 @@ <h1>What I did after installing Manjaro xfce</h1>
 <p>Although Manjaro comes packed with most of the apps that I would install on any other distro
 anyway: GIMP, LibreOffice, Steam, etc., here are some additional steps I took to make it rock.</p>
 <section id="If-you-get-a-default-xfce-environment-after-setup">
-<h2>If you get a default xfce environment after setup…</h2>
+<h2>If you get a default xfce environment after setup…<a href="#If-you-get-a-default-xfce-environment-after-setup" class="heading-link">#</a></h2>
 <p>It happened to me when I tried to mount my existing <code>/home</code> partition. Instead of the beautiful
 screenshot featured on Manjaro’s home page, I got something like this (image courtesy of Xfce
 project website):</p>
@@ -64,7 +64,7 @@ <h2>If you get a default xfce environment after setup…</h2>
 <p>Then restart your computer and see if it worked (it should).</p>
 </section>
 <section id="Get-Mirosoft-fonts">
-<h2>Get Mirosoft fonts</h2>
+<h2>Get Mirosoft fonts<a href="#Get-Mirosoft-fonts" class="heading-link">#</a></h2>
 <p>Getting Micro$oft fonts is like the first thing to do after any Linux distro installation. The Arch
 community has a whole <a href="https://wiki.archlinux.org/index.php/MS_Fonts">wiki page</a> dedicated to it. It’s worth mentioning that you can’t
 <em>legally</em> install those packages without the actual fonts already on your computer. Assuming you
@@ -77,12 +77,12 @@ <h2>Get Mirosoft fonts</h2>
 </code></pre>
 </section>
 <section id="Proper-font-smoothing">
-<h2>Proper font smoothing</h2>
+<h2>Proper font smoothing<a href="#Proper-font-smoothing" class="heading-link">#</a></h2>
 <p>I won’t try to reinvent the wheels here. Head to Manjaro’s <a href="http://wiki.manjaro.org/index.php?title=Improve_Font_Rendering">wiki page on font smoothing</a>.
 They’ve got everything you need.</p>
 </section>
 <section id="Install-international-fonts">
-<h2>Install international fonts</h2>
+<h2>Install international fonts<a href="#Install-international-fonts" class="heading-link">#</a></h2>
 <p>Even if you’re not Japanese or Korean, you’ll occasionally come across content that contains
 characters from these languages. With the default installation, all those characters will be shown
 as rectangles, which bugs me a lot.</p>
diff --git a/mcross/index.html b/mcross/index.html
index 81750db..1f49b58 100644
--- a/mcross/index.html
+++ b/mcross/index.html
@@ -69,7 +69,7 @@ <h1>Introducing McRoss—a minimal gemini browser</h1>
 desktop GUI application. For the rest of this blog post I try to elaborate on
 my idea of a <em>user-friendly desktop GUI application</em>.</p>
 <section id="Visual-feedback">
-<h3>Visual feedback:</h3>
+<h3>Visual feedback:<a href="#Visual-feedback" class="heading-link">#</a></h3>
 <p>When I click a button, visit a link, or press Enter on the address bar, I
 expect some kind of visual feedback that tells me my input registered
 correctly, and the browser is working on my request, not hanging. This sounds
@@ -82,7 +82,7 @@ <h3>Visual feedback:</h3>
 should the program hang or crash without displaying a proper message.</p>
 </section>
 <section id="Aesthetics">
-<h3>Aesthetics:</h3>
+<h3>Aesthetics:<a href="#Aesthetics" class="heading-link">#</a></h3>
 <p>Call me picky but I don’t like how in Castor links are presented as buttons and
 they don’t even have breathing room between them:</p>
 <p><img alt="Castor links" src="mcross_02_castor.png"></p>
@@ -105,7 +105,7 @@ <h3>Aesthetics:</h3>
 write one. I lifted this idea off of <a href="https://4chan.org/">imageboards</a> and <a href="https://textboard.org/">textboards</a>.</p>
 </section>
 <section id="Installation">
-<h3>Installation:</h3>
+<h3>Installation:<a href="#Installation" class="heading-link">#</a></h3>
 <p>Castor is written in Rust. One of Rust’s strong points is the ability to
 compile to a single statically linked executable that users can just download
 and run. Unfortunately, Castor doesn’t currently provide those compiled
@@ -119,7 +119,7 @@ <h3>Installation:</h3>
 improve the situation with “frozen” executables some time down the line.</p>
 </section>
 <section id="Closing-thoughts">
-<h1>Closing thoughts</h1>
+<h1>Closing thoughts<a href="#Closing-thoughts" class="heading-link">#</a></h1>
 <p>To me the whole gemini ecosystem represents the long-lost naive optimism of an
 earlier web ecosystem. It was not even as far as the “good old
 gopher/bbs days” those boomers keep ranting about - it was the days of early
diff --git a/movie-streaming/gflick-fixed/index.html b/movie-streaming/gflick-fixed/index.html
index 76cd4ab..f5e4243 100644
--- a/movie-streaming/gflick-fixed/index.html
+++ b/movie-streaming/gflick-fixed/index.html
@@ -90,7 +90,7 @@ <h1>Streaming videos from Google Drive: a second attempt</h1>
 </div>
 <p>Now that TLS is settled, I also made some changes to the usage flow:</p>
 <section id="Authentication">
-<h3>Authentication</h3>
+<h3>Authentication<a href="#Authentication" class="heading-link">#</a></h3>
 <p>The authentication responsibility has been moved from nginx/caddy to the python
 application itself to enable more fine-grained control:</p>
 <p>Every route, except for the video-serving <code>/v/*</code>, requires a <code>user_token</code> cookie.
@@ -108,7 +108,7 @@ <h3>Authentication</h3>
 be done for expiration mechanisms but the foundations are there.</p>
 </section>
 <section id="Aesthetics">
-<h3>Aesthetics</h3>
+<h3>Aesthetics<a href="#Aesthetics" class="heading-link">#</a></h3>
 <p>The web interface has been revamped to make it easier for <del>fat-fingered
 people on $current_year’s trendy stupidly thin</del> phones. Also present
 are folder icons and thumbnails, so it finally gives me everything I want from
@@ -117,7 +117,7 @@ <h3>Aesthetics</h3>
 <p><img alt="gflick screenshot" src="gflick_01_mobile.png"></p>
 </section>
 <section id="What-s-the-catch">
-<h2>What’s the catch?</h2>
+<h2>What’s the catch?<a href="#What-s-the-catch" class="heading-link">#</a></h2>
 <p><strong>Client device is solely responsible for decoding the raw file.</strong> This is both
 a blessing and a curse: We are guaranteed original quality but if the file was
 encoded with newer codecs (h265, av1, etc.) we’re stuck with inefficient
@@ -132,7 +132,7 @@ <h2>What’s the catch?</h2>
 dirty codebase is working fine for me so I’m in no hurry though…</p>
 </section>
 <section id="In-conclusion">
-<h2>In conclusion</h2>
+<h2>In conclusion<a href="#In-conclusion" class="heading-link">#</a></h2>
 <p>I’m happy with how things turned out: I have <a href="https://drive.google.com/">zero-maintenance unlimited cloud
 storage</a> for movies and an effortless streaming experience that requires
 virtually no client-side setup - just install a browser and streaming-capable
diff --git a/movie-streaming/gflick/index.html b/movie-streaming/gflick/index.html
index 30afa25..61599d1 100644
--- a/movie-streaming/gflick/index.html
+++ b/movie-streaming/gflick/index.html
@@ -101,7 +101,7 @@ <h1>Towards an acceptable video playing experience</h1>
 </li>
 </ul>
 <section id="Remote-seedbox-Google-Drive">
-<h2>Remote seedbox + Google Drive</h2>
+<h2>Remote seedbox + Google Drive<a href="#Remote-seedbox-Google-Drive" class="heading-link">#</a></h2>
 <p>I settled on Netflix and torrented stuff that’s not available there.
 For the seedbox, I installed Transmission-web on a Ramnode VPS that has 320GB
 of HDD at $50/year. The network bandwidth is meh but it gets the job done.</p>
@@ -124,7 +124,7 @@ <h2>Remote seedbox + Google Drive</h2>
 <p>The latter is not ideal.</p>
 </section>
 <section id="Enter-gflick">
-<h2>Enter gflick</h2>
+<h2>Enter gflick<a href="#Enter-gflick" class="heading-link">#</a></h2>
 <p>Turns out advanced video players like <code>mpv</code> and <code>vlc</code> can directly stream HTTP
 videos with full support for seeking and audio / text(a.k.a subtitles) tracks.
 See, well-formed video container formats will have metadata at the beginning of
@@ -159,7 +159,7 @@ <h2>Enter gflick</h2>
 those Chinese Surface knock-offs or what.</p>
 </section>
 <section id="Other-failed-attempts">
-<h2>Other failed attempts</h2>
+<h2>Other failed attempts<a href="#Other-failed-attempts" class="heading-link">#</a></h2>
 <ul>
 <li>
 <a href="https://github.com/nhanb/mpv-gdrive">mpv-gdrive</a>: Using mpv’s lua scripting API to automatically set the
diff --git a/node-webkit/index.html b/node-webkit/index.html
index 3046d3a..7fc1561 100644
--- a/node-webkit/index.html
+++ b/node-webkit/index.html
@@ -64,7 +64,7 @@ <h1>Setting up your development environment for a node-webkit project</h1>
 </li>
 </ul>
 <section id="Install-node-webkit-on-your-machine">
-<h2>Install node-webkit on your machine</h2>
+<h2>Install node-webkit on your machine<a href="#Install-node-webkit-on-your-machine" class="heading-link">#</a></h2>
 <p>Follow the README on <a href="https://github.com/rogerwang/node-webkit">node-webkit’s GitHub page</a> to download a precompiled <code>nw</code> binary for your
 platform. If you’re using Arch Linux, you’re in luck since there’s already an AUR package:</p>
 <pre><code class="language-sh">$ yaourt -S node-webkit
@@ -74,7 +74,7 @@ <h2>Install node-webkit on your machine</h2>
 <p>The rest of this tutorial will assume that you have <code>nw</code> accessible as an executable in your $PATH.</p>
 </section>
 <section id="Running-an-app">
-<h2>Running an app</h2>
+<h2>Running an app<a href="#Running-an-app" class="heading-link">#</a></h2>
 <p>First, take a look at nw’s <a href="https://github.com/rogerwang/node-webkit#quick-start">quickstart guide</a>. We’ll make a somewhat different structure,
 allowing the <strong>dist</strong> directory to store our binary releases:</p>
 <pre><code>├── app/
@@ -127,7 +127,7 @@ <h2>Running an app</h2>
 <p>But let’s not get ahead of ourselves. Let’s solve the most obvious dev issue first:</p>
 </section>
 <section id="Automatic-reload">
-<h2>Automatic reload</h2>
+<h2>Automatic reload<a href="#Automatic-reload" class="heading-link">#</a></h2>
 <p>Sure enough, at first glance your app is just another html page. You may be tempted to run some
 simple http server and open localhost in Google Chrome (<code>python2 -m SimpleHTTPServer 8080</code>
 anyone?). There are tons of ways to make Google Chrome automatically reload a page, right?</p>
@@ -177,7 +177,7 @@ <h2>Automatic reload</h2>
 </code></pre>
 </section>
 <section id="Simple-cross-platform-build-command">
-<h2>Simple cross-platform build command</h2>
+<h2>Simple cross-platform build command<a href="#Simple-cross-platform-build-command" class="heading-link">#</a></h2>
 <p>To be honest, you can manually write shell scripts to build for each platform. Check out <a href="https://github.com/rogerwang/node-webkit/wiki/How-to-package-and-distribute-your-apps">this wiki
 article</a> if you prefer the do-it-yourself way.</p>
 <p>But if you’re lazy (like me) and don’t have a problem using nodejs/grunt, just use the excellent
diff --git a/notes.html b/notes.html
index 6b6a775..9558e2e 100644
--- a/notes.html
+++ b/notes.html
@@ -36,7 +36,7 @@ <h1>Random notes</h1>
 
 <p>In which I jot down scattered tidbits on various topics.</p>
 <section id="SRE">
-<h2>SRE</h2>
+<h2>SRE<a href="#SRE" class="heading-link">#</a></h2>
 <p>There’s a good <a href="http://rachelbythebay.com/w/2019/07/21/reliability/">checklist</a>
 on Rachel By The Bay, the gist is:</p>
 <ul>
diff --git a/pathogen-vs-vundle/index.html b/pathogen-vs-vundle/index.html
index aa0daea..ce0eb10 100644
--- a/pathogen-vs-vundle/index.html
+++ b/pathogen-vs-vundle/index.html
@@ -45,13 +45,13 @@ <h1>Modern vim plugin management: Pathogen vs Vundle</h1>
 third-party tools: Pathogen or Vundle. I assume you are using a Linux distro and have git
 already installed. If not, consult Dr. Google for more details.</p>
 <section id="Vim-plugins-anatomy">
-<h2>Vim plugins anatomy</h2>
+<h2>Vim plugins anatomy<a href="#Vim-plugins-anatomy" class="heading-link">#</a></h2>
 <p>A vim plugin is simply a set of files that alter vim’s behavior or add new functionalities to it.
 To make this possible, by default vim looks for files in your home folder (which is 
 <code>/home/username</code> or <code>~</code>):</p>
 </section>
 <section id="vimrc-file">
-<h2>~/.vimrc (file)</h2>
+<h2>~/.vimrc (file)<a href="#vimrc-file" class="heading-link">#</a></h2>
 <p>This is where you put your personalizations to vim: indentations, keybindings, etc. This post
 will not discuss in detail how you do your customizations. For now just know that it’s there.</p>
 <p>You will probably want to move this file into your ~/.vim folder to be able to manage everything
@@ -61,7 +61,7 @@ <h2>~/.vimrc (file)</h2>
 </code></pre>
 </section>
 <section id="vim-directory">
-<h2>~/.vim (directory)</h2>
+<h2>~/.vim (directory)<a href="#vim-directory" class="heading-link">#</a></h2>
 <p>This should contain a bunch of subdirectories. Some examples:</p>
 <ul>
 <li>
@@ -137,7 +137,7 @@ <h2>~/.vim (directory)</h2>
 pretty sight now, is it?</p>
 </section>
 <section id="Pathogen-to-the-rescue">
-<h2>Pathogen to the rescue!</h2>
+<h2>Pathogen to the rescue!<a href="#Pathogen-to-the-rescue" class="heading-link">#</a></h2>
 <p>The legendary Tim Pope came up with a genius solution:
 <a href="https://github.com/tpope/vim-pathogen">pathogen</a>.
 Now let’s install it like any regular plugin (I’ve omitted the README):</p>
@@ -183,7 +183,7 @@ <h2>Pathogen to the rescue!</h2>
 becomes trivial: just remove or update its own directory.</p>
 </section>
 <section id="Pathogen-Git">
-<h2>Pathogen + Git</h2>
+<h2>Pathogen + Git<a href="#Pathogen-Git" class="heading-link">#</a></h2>
 <p>Everything goes to the cloud these days, and certainly your vim setup should as well. If you
 haven’t created a <a href="https://github.com">Github</a> account, do it now. Create an empty repository
 with any name you want (mine is <code>.vim</code>). Don’t commit yet. Create a file: <code>~/.vim/.gitignore</code>,
@@ -238,7 +238,7 @@ <h2>Pathogen + Git</h2>
 for detailed instructions.</p>
 </section>
 <section id="Vundle-the-new-cool-kid">
-<h2>Vundle, the new cool kid</h2>
+<h2>Vundle, the new cool kid<a href="#Vundle-the-new-cool-kid" class="heading-link">#</a></h2>
 <p>This time let’s start fresh: remove all submodules and pathogen. Your bundle folder should be
 now empty. Clone <a href="https://github.com/gmarik/vundle">Vundle</a>:</p>
 <pre><code class="language-sh">git clone https://github.com/gmarik/vundle.git ~/.vim/bundle/vundle
diff --git a/petition-fraud/index.html b/petition-fraud/index.html
index b967151..497c553 100644
--- a/petition-fraud/index.html
+++ b/petition-fraud/index.html
@@ -44,7 +44,7 @@ <h1>I did NOT sign that online petition!</h1>
 <p><img alt="" src="rmitsc_01_wtf.png"></p>
 <p>Um… I don’t remember signing any petition recently (or ever, for that matter)?</p>
 <section id="What-happened">
-<h2>What happened?</h2>
+<h2>What happened?<a href="#What-happened" class="heading-link">#</a></h2>
 <p>Apparently someone used my RMIT student email address to sign some petition for disbanding the
 university’s Student Council. Said petition was apparently started by some Ms. Trần Ngọc Tuệ Mẫn -
 Student Council’s vice president. Well… yay for free speech, I guess? Anyway, my name was really
@@ -60,7 +60,7 @@ <h2>What happened?</h2>
 us even having to know what it’s all about. Gee, thanks!</p>
 </section>
 <section id="Why-do-I-even-care">
-<h2>Why do I even care?</h2>
+<h2>Why do I even care?<a href="#Why-do-I-even-care" class="heading-link">#</a></h2>
 <p>I just don’t like people using my name without my consent. More importantly, I have my reasons to
 disagree with the sentiments expressed in her petition description. Also I thought this could be a
 somewhat useful public service announcement, or a mildly entertaining daily wtf story. I don’t
diff --git a/pippable-webapp/index.html b/pippable-webapp/index.html
index fe1caa2..6d8bfea 100644
--- a/pippable-webapp/index.html
+++ b/pippable-webapp/index.html
@@ -54,7 +54,7 @@ <h1>I made my python webapp installable via pip</h1>
 </code></pre>
 <p>So how does that work? Let’s break it down.</p>
 <section id="The-pytaku-executables">
-<h2>The pytaku-* executables</h2>
+<h2>The pytaku-* executables<a href="#The-pytaku-executables" class="heading-link">#</a></h2>
 <p><a href="https://python-poetry.org/">Poetry</a> is awesome. Not only does it offer sane dependency management that
 plays well with the pyenv + virtualenv combo, but it also vastly simplifies
 building and publishing python libraries. Telling pip to install executables
@@ -99,7 +99,7 @@ <h2>The pytaku-* executables</h2>
 </ul>
 </section>
 <section id="But-why-bother">
-<h2>But why bother?</h2>
+<h2>But why bother?<a href="#But-why-bother" class="heading-link">#</a></h2>
 <p>Lincoln Loop’s series of Django-related blog posts were my main inspiration.
 Central to this idea is <a href="https://lincolnloop.com/blog/using-setuppy-your-django-project/">Using setup.py in Your (Django) Project</a>, which
 explains both how and why you would want to make your python project
diff --git a/projects/index.html b/projects/index.html
index e34287c..41e527a 100644
--- a/projects/index.html
+++ b/projects/index.html
@@ -37,20 +37,20 @@ <h1>Projects</h1>
 <p>I’ve written some open source software in my free time.
 In some cases I even maintain them! Shocking, I know.</p>
 <section id="s4g">
-<h2>s4g</h2>
+<h2>s4g<a href="#s4g" class="heading-link">#</a></h2>
 <p><a href="../s4g/">s4g</a> is the static site generator that powers this blog.
 It’s written in Go and uses djot as a saner markdown. Reasonably complete, but
 definitely has room for improvement—internal link rot detection is one strong
 candidate. Syntax highlighting would be nice too.</p>
 </section>
 <section id="Shark">
-<h2>Shark</h2>
+<h2>Shark<a href="#Shark" class="heading-link">#</a></h2>
 <p><a href="https://github.com/nhanb/shark/">Shark</a> is a simple “desktop pet”. It’s my first foray into cross-platform
 desktop application development, also my first Go project that actually did
 anything fun.</p>
 </section>
 <section id="Pytaku">
-<h2>Pytaku</h2>
+<h2>Pytaku<a href="#Pytaku" class="heading-link">#</a></h2>
 <p><a href="https://git.sr.ht/~nhanb/pytaku">Pytaku</a> is a web-based manga reader that’s designed to be as
 self-host-friendly as possible. It’s an experiment to see how far I can get
 with a simpler toolset: basically just flask, sqlite and mithril.js for the
@@ -58,7 +58,7 @@ <h2>Pytaku</h2>
 so it’s currently in maintenance mode.</p>
 </section>
 <section id="ORTS">
-<h2>ORTS</h2>
+<h2>ORTS<a href="#ORTS" class="heading-link">#</a></h2>
 <p><a href="https://github.com/nhanb/orts/">ORTS</a> is a GUI for operating scoreboards on
 fighting game streams. It’s served its purpose beaufifully during
 <a href="https://www.facebook.com/SaigonFGC/">our</a> local tournaments and several
@@ -66,20 +66,20 @@ <h2>ORTS</h2>
 happened.</p>
 </section>
 <section id="GORTS">
-<h2>GORTS</h2>
+<h2>GORTS<a href="#GORTS" class="heading-link">#</a></h2>
 <p><a href="https://github.com/nhanb/gorts/">GORTS</a> is ORTS rewritten in Go and Tcl/Tk.
 It has been a fun experiment in cross-language IPC, and a much less masochistic
 exercise in software packaging compared to Python.</p>
 </section>
 <section id="An-animated-wallpaper-for-KDE-Plasma">
-<h2>An animated wallpaper for KDE Plasma</h2>
+<h2>An animated wallpaper for KDE Plasma<a href="#An-animated-wallpaper-for-KDE-Plasma" class="heading-link">#</a></h2>
 <p>I was frustrated that KDE 5 didn’t allow animated gifs as wallpaper so I <a href="https://github.com/nhanb/com.nerdyweekly.animated">wrote
 one</a>. Every now and then someone would <a href="https://www.reddit.com/r/unixporn/comments/9sd5uy/kde_plasma_blur_gif_pixel_art_wallpaper_look/">use it on /r/unixporn</a> and cause
 a surge in github stars. It is currently my most starred repo, which gives me
 mixed feelings.</p>
 </section>
 <section id="McRoss">
-<h2>McRoss</h2>
+<h2>McRoss<a href="#McRoss" class="heading-link">#</a></h2>
 <p><a href="https://sr.ht/~nhanb/mcross">McRoss</a> is a minimal and usable
 <a href="https://gemini.circumlunar.space/">gemini://</a> browser written in python and
 tkinter, meaning it Just Works (tm) on any self-respecting desktop OS.
@@ -89,7 +89,7 @@ <h2>McRoss</h2>
 much shelved.</p>
 </section>
 <section id="Caophim">
-<h2>Caophim</h2>
+<h2>Caophim<a href="#Caophim" class="heading-link">#</a></h2>
 <p><a href="https://github.com/nhanb/caophim">Caophim</a> is my take on imageboard software and also my excuse to try
 <a href="https://nim-lang.org/">Nim</a>. There’s a usable live instance at <a href="https://caophim.imnhan.com/">caophim.imnhan.com</a>. It’s
 nowhere near my goals but to be honest I got tired of <a href="https://github.com/nim-lang/Nim/issues/13531">running</a> <a href="https://github.com/nim-lang/Nim/issues/13986">into</a>
diff --git a/pytaku-old/index.html b/pytaku-old/index.html
index 7945f77..2005538 100644
--- a/pytaku-old/index.html
+++ b/pytaku-old/index.html
@@ -49,7 +49,7 @@ <h1>Introducing Pytaku—the only online manga reader you&#39;ll ever need</h1>
 manga sites, giving you one single place to keep track of your reading progress and watch for new
 chapters with ease. Here are some of the features implemented so far:</p>
 <section id="Lightning-fast-ad-free-reading-experience">
-<h2>Lightning fast, ad-free reading experience</h2>
+<h2>Lightning fast, ad-free reading experience<a href="#Lightning-fast-ad-free-reading-experience" class="heading-link">#</a></h2>
 <p>All pages in a chapter are loaded at once, unlike most other sites that only let you view one page
 at a time, forcing you to reload their distracting advertisements and disrupt you flow (especially
 for people with not-so-fast internet connection).</p>
@@ -58,40 +58,40 @@ <h2>Lightning fast, ad-free reading experience</h2>
 instantly.</p>
 </section>
 <section id="Keep-track-of-your-reading-progress-Automatically">
-<h2>Keep track of your reading progress. Automatically.</h2>
+<h2>Keep track of your reading progress. Automatically.<a href="#Keep-track-of-your-reading-progress-Automatically" class="heading-link">#</a></h2>
 <p>Each logged in user will have a nice badge on each chapter showing their progress: (keeping a
 chapter page open for a few seconds registers it as “reading”, and scrolling to the bottom marks it
 as “finished”)</p>
 <p><img alt="Chapter progress badge" src="pytaku_01_chapter_progress.png"></p>
 </section>
 <section id="Bookmark-series-to-watch-for-updates">
-<h2>Bookmark series to watch for updates</h2>
+<h2>Bookmark series to watch for updates<a href="#Bookmark-series-to-watch-for-updates" class="heading-link">#</a></h2>
 <p>Maintain a list of series so you can have one single place to find out whether there are new
 chapters for the series you love.</p>
 <p><img alt="Bookmarked series" src="pytaku_02_bookmarked_series.png"></p>
 </section>
 <section id="English-Vietnamese-and-support-for-other-languages">
-<h2>English, Vietnamese and support for other languages</h2>
+<h2>English, Vietnamese and support for other languages<a href="#English-Vietnamese-and-support-for-other-languages" class="heading-link">#</a></h2>
 <p>Pytaku comes in English by default and configurable to be in Vietnamese. If you want to translate
 it to your own language, feel free to follow the example from the <a href="https://github.com/nhanb/pytaku-old-gae/blob/master/frontend/languages/en.yaml">English language file</a> and
 send me a pull request.</p>
 <p><img alt="Vietnamese interface" src="pytaku_03_vietnamese.png"></p>
 </section>
 <section id="Open-source-and-free-to-run-your-own-site">
-<h2>Open source and free to run your own site</h2>
+<h2>Open source and free to run your own site<a href="#Open-source-and-free-to-run-your-own-site" class="heading-link">#</a></h2>
 <p>Pytaku’s source code is released under the free-as-in-freedom <a href="https://www.gnu.org/licenses/quick-guide-gplv3.html">GPLv3</a> and <a href="https://github.com/nhanb/pytaku-old-gae/">put on GitHub</a>.
 Since it’s written to be run on Google App Engine which is free for small sites, tech-savvy people
 can set up their own private pytaku clone in a few minutes. Check out the <a href="https://github.com/nhanb/pytaku-old-gae/blob/master/README.markdown">README file</a> for
 instructions.</p>
 </section>
 <section id="Open-to-suggestions-and-hopefully-contructive-criticism">
-<h2>Open to suggestions and (hopefully contructive) criticism</h2>
+<h2>Open to suggestions and (hopefully contructive) criticism<a href="#Open-to-suggestions-and-hopefully-contructive-criticism" class="heading-link">#</a></h2>
 <p>Want another manga site to be included as source? Need a feature that you think many others can
 benefit from? Feel free to open an issue on GitHub, or give me a shout on the official <a href="https://gitter.im/nhanb/pytaku">support
 chat room</a>.</p>
 </section>
 <section id="Give-it-a-spin">
-<h2>Give it a spin</h2>
+<h2>Give it a spin<a href="#Give-it-a-spin" class="heading-link">#</a></h2>
 <p><del>Click here to go to the app. Have fun! :)</del> Update: this version of pytaku is
 no longer online.</p>
 </section>
diff --git a/rmit-wifi/index.html b/rmit-wifi/index.html
index 84ef817..5d85c19 100644
--- a/rmit-wifi/index.html
+++ b/rmit-wifi/index.html
@@ -41,7 +41,7 @@
 <h1>Fix RMIT wi-fi issue in Ubuntu 13.04 and variants</h1>
 
 <section id="The-issue">
-<h2>The issue</h2>
+<h2>The issue<a href="#The-issue" class="heading-link">#</a></h2>
 <p>When I upgraded to Xubuntu 13.04, although I could connect to any other wi-fi network painlessly,
 the RMIT-WPA network just never allowed me to establish a connection. The most annoying part was
 that it had been working fine in previous versions (12.04, 12.10).</p>
@@ -51,7 +51,7 @@ <h2>The issue</h2>
 <p><img alt="RMIT wi-fi settings" src="rmit_wifi.png"></p>
 </section>
 <section id="The-solution">
-<h2>The solution</h2>
+<h2>The solution<a href="#The-solution" class="heading-link">#</a></h2>
 <p>Just manually edit <code>/etc/NetworkManager/system-connections/RMIT-WPA</code>, make sure that you have
 <code>system-ca-certs=false</code>, then restart the wifi connection. To edit this file you will need root
 permission. If you’re not sure how to do this, open a terminal and enter this command to open
@@ -63,7 +63,7 @@ <h2>The solution</h2>
 many have complained about it. There seems to be no developer assigned to fix it though. I’ll keep
 you updated on the issue.</p>
 <section id="Update-Dec-16-2013">
-<h3>Update (Dec 16, 2013)</h3>
+<h3>Update (Dec 16, 2013)<a href="#Update-Dec-16-2013" class="heading-link">#</a></h3>
 <p>A fix has been released in GNOME upstream but not incorporated into official Ubuntu repositories
 yet. An impatient contributor has created his own PPA to provide the fixed package. To install it,
 enter the following commands:</p>
diff --git a/s4g/index.html b/s4g/index.html
index 1c2648d..13b08e4 100644
--- a/s4g/index.html
+++ b/s4g/index.html
@@ -43,7 +43,7 @@ <h1>s4g is a Stupidly Simple Static Site Generator</h1>
 
 <p>This blog is now built with <a href="https://github.com/nhanb/s4g">s4g</a>—an experimental static site generator.</p>
 <section id="Why">
-<h2>Why?</h2>
+<h2>Why?<a href="#Why" class="heading-link">#</a></h2>
 <p>Unlike typical static site generators, s4g doesn’t separate source code and
 output. For a concrete example, <a href="https://getpelican.com/">Pelican</a> would read source from the
 /content/ dir and write a complete website to /output/, like this:</p>
@@ -90,7 +90,7 @@ <h2>Why?</h2>
 shebang? One thing led to another, and s4g was born.</p>
 <p>This dumb approach to paths simplifies a few things:</p>
 <section id="1-Folder-layout-is-website-layout">
-<h3>1. Folder layout <em>is</em> website layout</h3>
+<h3>1. Folder layout <em>is</em> website layout<a href="#1-Folder-layout-is-website-layout" class="heading-link">#</a></h3>
 <p>With Pelican we typically need to supply a few url patterns to let
 the generator know where to put the generated html:</p>
 <pre><code>ARTICLE_PATHS = ["posts"]
@@ -102,7 +102,7 @@ <h3>1. Folder layout <em>is</em> website layout</h3>
 <p>While with s4g we just put the .dj file where we want the .html to end up.</p>
 </section>
 <section id="2-Obvious-static-asset-placement">
-<h3>2. Obvious static asset placement</h3>
+<h3>2. Obvious static asset placement<a href="#2-Obvious-static-asset-placement" class="heading-link">#</a></h3>
 <p>A post typically links to static assets such as images or videos.
 Ideally we want to put these assets in the same place as their content file.
 This is surprisingly tricky with Pelican: we have to either throw every
@@ -136,7 +136,7 @@ <h3>2. Obvious static asset placement</h3>
 support in the first place).</p>
 </section>
 <section id="3-Easy-post-grouping">
-<h3>3. Easy post grouping</h3>
+<h3>3. Easy post grouping<a href="#3-Easy-post-grouping" class="heading-link">#</a></h3>
 <p>Sometimes I write a series of related posts, and want to make it obvious when
 a visitor lands on any such post. The s4g solution: put them all in a folder.
 Here’s a <a href="https://hi.imnhan.com/movie-streaming/">live example</a> from my blog:</p>
@@ -174,7 +174,7 @@ <h3>3. Easy post grouping</h3>
 <p><img alt="" src="series-footer.png"></p>
 </section>
 <section id="Miscellaneous-blog-things">
-<h3>Miscellaneous blog things</h3>
+<h3>Miscellaneous blog things<a href="#Miscellaneous-blog-things" class="heading-link">#</a></h3>
 <p>An RSS feed is table stakes, so that’s what we get out of the box.
 Technically it’s an Atom feed, but hey, tomaytoes-tomahtoes.</p>
 <p>There’s also automatic OpenGraph / Twitter Cards meta tags, so we get those
@@ -191,7 +191,7 @@ <h3>Miscellaneous blog things</h3>
 short, typeable paths such as “mcross”.</p>
 </section>
 <section id="Quality-of-life-stuff">
-<h3>Quality-of-life stuff</h3>
+<h3>Quality-of-life stuff<a href="#Quality-of-life-stuff" class="heading-link">#</a></h3>
 <p>I originally wanted to use s4g as a teaching tool for a sort of “Learning
 HTML/CSS by creating a blog” tutorial series, but lost interest halfway. It
 still resulted in a few user-friendly goodies: out-of-the box livereload,
@@ -203,7 +203,7 @@ <h3>Quality-of-life stuff</h3>
 </section>
 </section>
 <section id="Quickstart">
-<h2>Quickstart</h2>
+<h2>Quickstart<a href="#Quickstart" class="heading-link">#</a></h2>
 <pre><code class="language-sh"># Install
 sudo pacman -Syu nodejs go
 go install go.imnhan.com/s4g@latest
@@ -227,9 +227,9 @@ <h2>Quickstart</h2>
 fallback thumbnails, per-post custom templates, series, among other things.</p>
 </section>
 <section id="Questionables">
-<h2>Questionables</h2>
+<h2>Questionables<a href="#Questionables" class="heading-link">#</a></h2>
 <section id="Djot">
-<h3>Djot?</h3>
+<h3>Djot?<a href="#Djot" class="heading-link">#</a></h3>
 <p>Imagine markdown but not full of corner cases and highly extensible.
 No disrespect to John Gruber here—his thing worked for his purposes—but
 I think it’s a tragedy that we as an industry failed to converge on a more
@@ -237,13 +237,13 @@ <h3>Djot?</h3>
 </section>
 <section id="Since-source-and-output-live-in-the-same-folder-will-some-bug-in-s4g-eat-my-source">
 <h3>Since source and output live in the same folder, will some bug in s4g eat
-my source?</h3>
+my source?<a href="#Since-source-and-output-live-in-the-same-folder-will-some-bug-in-s4g-eat-my-source" class="heading-link">#</a></h3>
 <p>I obviously try to avoid that, but the current code is proof-of-concept
 quality. Therefore, I only run s4g on version-controlled websites, and
 recommend users do the same for now.</p>
 </section>
 <section id="Adding-compiled-assets-to-version-control-sounds-bad">
-<h3>Adding compiled assets to version control sounds… bad?</h3>
+<h3>Adding compiled assets to version control sounds… bad?<a href="#Adding-compiled-assets-to-version-control-sounds-bad" class="heading-link">#</a></h3>
 <p>For programs, probably.
 For websites though, I prefer to keep snapshots of the whole website as it’s
 deployed, and be able to roll it back together with the source code. Admittedly
@@ -252,7 +252,7 @@ <h3>Adding compiled assets to version control sounds… bad?</h3>
 </section>
 <section id="I-just-looked-at-the-code-and-holy-crap-what-s-with-all-those-non-data-structures-and-nested-loops">
 <h3>I just looked at the code and holy crap what’s with all those (non) data
-structures and nested loops!</h3>
+structures and nested loops!<a href="#I-just-looked-at-the-code-and-holy-crap-what-s-with-all-those-non-data-structures-and-nested-loops" class="heading-link">#</a></h3>
 <p>Yeah but the whole thing takes about 100ms on my thirty-something-articles
 blog, which is by no means impressive, but not a real hindrance for daily use
 either. If an actual user shows up with a site big enough to have a problem,
diff --git a/sqlite-python/index.html b/sqlite-python/index.html
index 56dc933..b68b082 100644
--- a/sqlite-python/index.html
+++ b/sqlite-python/index.html
@@ -56,11 +56,11 @@ <h1>Working with SQLite in Python without an ORM or migration framework</h1>
 can trivially implement. Obviously, I had to try it out on my latest <a href="https://sr.ht/~nhanb/pytaku/">pet
 project</a>. Here I’ll outline some of my findings.</p>
 <section id="APSW-as-the-driver">
-<h2>APSW as the driver</h2>
+<h2>APSW as the driver<a href="#APSW-as-the-driver" class="heading-link">#</a></h2>
 <p>Using <a href="https://rogerbinns.github.io/apsw/">apsw</a> instead of the standard library’s sqlite3 package has a couple
 of advantages:</p>
 <section id="It-s-easy-to-link-against-the-latest-sqlite3-version">
-<h3>It’s easy to link against the latest sqlite3 version</h3>
+<h3>It’s easy to link against the latest sqlite3 version<a href="#It-s-easy-to-link-against-the-latest-sqlite3-version" class="heading-link">#</a></h3>
 <p>I originally ran pytaku on a cheap Vietnamese VPS provider, which only offered
 Ubuntu 12.04. This came with a relatively old sqlite3 version that lacked
 UPSERT support (probably among other things that I forgot). I guess it’s
@@ -71,7 +71,7 @@ <h3>It’s easy to link against the latest sqlite3 version</h3>
 python)</p>
 </section>
 <section id="It-has-the-same-defaults-as-upstream-sqlite">
-<h3>It has the same defaults as upstream sqlite</h3>
+<h3>It has the same defaults as upstream sqlite<a href="#It-has-the-same-defaults-as-upstream-sqlite" class="heading-link">#</a></h3>
 <p>Python stdlib’s <code>sqlite3</code> has a few default configurations that deviate from
 sqlite’s. A couple of things that actually bit me are:</p>
 <ul>
@@ -94,7 +94,7 @@ <h3>It has the same defaults as upstream sqlite</h3>
 </section>
 </section>
 <section id="A-minimum-viable-DB-migration-scheme">
-<h2>A minimum viable DB migration scheme</h2>
+<h2>A minimum viable DB migration scheme<a href="#A-minimum-viable-DB-migration-scheme" class="heading-link">#</a></h2>
 <p>With <a href="https://github.com/nhanb/pytaku/blob/65a6c08128ebbc2b7d33a6b043798c69ac7dfebe/src/pytaku/database/migrator.py">&lt;100 lines</a> of python, I ended up with a migrator that:</p>
 <ul>
 <li>
@@ -177,7 +177,7 @@ <h2>A minimum viable DB migration scheme</h2>
 projects that have good documentation, who would have thought?</p>
 </section>
 <section id="Recommended-sane-defaults">
-<h2>Recommended sane defaults</h2>
+<h2>Recommended sane defaults<a href="#Recommended-sane-defaults" class="heading-link">#</a></h2>
 <p>SQLite comes with some default settings that may be surprising for people
 coming from e.g. Postgres. Here are some tweaks that worked better for me.</p>
 <p><a href="https://sqlite.org/wal.html">Enable WAL mode</a>. This allows for concurrent readers, which is usually
@@ -192,7 +192,7 @@ <h2>Recommended sane defaults</h2>
 matter how short the wait is.</p>
 </section>
 <section id="A-quick-note-on-SQL-injection">
-<h2>A quick note on SQL injection</h2>
+<h2>A quick note on SQL injection<a href="#A-quick-note-on-SQL-injection" class="heading-link">#</a></h2>
 <p><em>(or how to move on from the late 90s)</em></p>
 <p>You don’t need a full blown ORM to protect yourself against SQL injections. In
 fact, SQLite (and any sane RDBMS really) has built-in support for it called
diff --git a/tmux-italics/index.html b/tmux-italics/index.html
index 4bc33b5..47d9591 100644
--- a/tmux-italics/index.html
+++ b/tmux-italics/index.html
@@ -60,7 +60,7 @@ <h1>Enable italic text inside vim inside tmux inside gnome-terminal</h1>
 Let’s change that. My current setup is terminal vim running inside a tmux session on
 gnome-terminal. Let’s go through these things.</p>
 <section id="gnome-terminal">
-<h2>gnome-terminal</h2>
+<h2>gnome-terminal<a href="#gnome-terminal" class="heading-link">#</a></h2>
 <p>Note that older versions of <code>gnome-terminal</code> do not support italic text. To check if your terminal
 does support it, run this command:</p>
 <pre><code class="language-sh">$ echo -e "\e[3m foo \e[23m"
@@ -68,7 +68,7 @@ <h2>gnome-terminal</h2>
 <p>If your version of gnome-terminal supports it, an italic <em>foo</em> will appear. If not, upgrade it! :)</p>
 </section>
 <section id="vim">
-<h2>vim</h2>
+<h2>vim<a href="#vim" class="heading-link">#</a></h2>
 <p>You may have noticed: <code>[3m</code> and <code>[23m</code> are the special sequences to start and stop printing
 italic text. Unfortunately, vim doesn’t care about those. It expects <code>sitm</code> and <code>ritm</code> instead.
 We’ll need to map them manually. Simply use these commands:</p>
@@ -84,7 +84,7 @@ <h2>vim</h2>
 not, I can’t help you any further :P</p>
 </section>
 <section id="tmux">
-<h2>tmux</h2>
+<h2>tmux<a href="#tmux" class="heading-link">#</a></h2>
 <p>The only reason I use terminal vim instead of gvim is tmux integration, therefore I almost always
 run vim inside a tmux session. Unfortunately tmux does some weird things to your terminal, one of
 them is altering the <code>$TERM</code> environment variable. When we open a tmux session, it will typically
@@ -96,14 +96,14 @@ <h2>tmux</h2>
 </code></pre>
 </section>
 <section id="More-on-vim">
-<h2>More on vim</h2>
+<h2>More on vim<a href="#More-on-vim" class="heading-link">#</a></h2>
 <p>If you still can’t see any italic text in a markdown file, it might be because your colorscheme
 deliberately disables it. Try using another colorscheme (I highly recommend <a href="http://ethanschoonover.com/solarized">solarized</a>). You
 can also check if your markdown syntax plugin does use italics; I’m currently using <a href="https://github.com/tpope/vim-markdown">Tim Pope’s
 markdown plugin</a> and it works great!</p>
 </section>
 <section id="References">
-<h2>References:</h2>
+<h2>References:<a href="#References" class="heading-link">#</a></h2>
 <ol>
 <li>
 <a href="http://superuser.com/questions/204743/terminal-that-supports-ansi-italic-escape-code">Terminal that supports ANSI italic escape code?</a>
diff --git a/ubuntu-programs/index.html b/ubuntu-programs/index.html
index e156574..b389cbb 100644
--- a/ubuntu-programs/index.html
+++ b/ubuntu-programs/index.html
@@ -44,7 +44,7 @@ <h1>Installing programs in Ubuntu</h1>
 a better understanding about Linux’s structure for storing and executing programs, ultimately
 appreciate the usefulness of package managers in general.</p>
 <section id="Executables">
-<h2>Executables</h2>
+<h2>Executables<a href="#Executables" class="heading-link">#</a></h2>
 <p>Let’s start with something simple. Fire up your favorite text editor and create a file called
 <code>itc.sh</code> with the following content:</p>
 <pre><code class="language-sh">#!/bin/bash
@@ -74,7 +74,7 @@ <h2>Executables</h2>
 </blockquote>
 </section>
 <section id="Path">
-<h2>Path</h2>
+<h2>Path<a href="#Path" class="heading-link">#</a></h2>
 <p>So we’ve created a program that shows a useless message, good job! However, every time we call
 it, we need to specify the whole address to the file: <code>~/Desktop/itc</code> is probably not a very cool
 looking command. In order to make it possible to simply run <code>itc</code>, you need to move it to the
@@ -90,7 +90,7 @@ <h2>Path</h2>
 </blockquote>
 </section>
 <section id="Packages">
-<h2>Packages</h2>
+<h2>Packages<a href="#Packages" class="heading-link">#</a></h2>
 <p>Unfortunately, most programs have a lot of files instead of one, and they are scattered to many
 different folders. Let’s have a look at the files of <code>wget</code> - the downloader that’s included in
 every major Linux distribution:</p>
@@ -158,7 +158,7 @@ <h2>Packages</h2>
 you’ve obtained the file, simply open it with Ubuntu Software Center to start installing.</p>
 </section>
 <section id="Synaptic-Ubuntu-Software-Center">
-<h2>Synaptic, Ubuntu Software Center</h2>
+<h2>Synaptic, Ubuntu Software Center<a href="#Synaptic-Ubuntu-Software-Center" class="heading-link">#</a></h2>
 <p>Aptitude is only a command-line program, which is not very user-friendly. Synaptic is a GUI program
 that provides a nice user interface that’s easy to use, while internally it uses <code>apt</code> to do all
 the actual work.</p>
diff --git a/vim-open-link/index.html b/vim-open-link/index.html
index 3b8e4c5..116231b 100644
--- a/vim-open-link/index.html
+++ b/vim-open-link/index.html
@@ -91,7 +91,7 @@ <h1>Opening http link under the cursor in vim</h1>
 a shell command composed from arbitrary, potentially unsafe input (i.e. text
 file content), don’t worry: that’s what <a href="https://learnvimscriptthehardway.stevelosh.com/chapters/32.html#escaping-shell-command-arguments"><code>shellescape()</code></a> is for.</p>
 <section id="But-why-stop-there">
-<h2>But why stop there?</h2>
+<h2>But why stop there?<a href="#But-why-stop-there" class="heading-link">#</a></h2>
 <p>We’re using Jira at work (I know, don’t ask), and we have a convention to
 include the Jira ticket in all top-level git commit messages like this (French
 optional):</p>
diff --git a/yaks.html b/yaks.html
index 2f3c03a..9ee6e29 100644
--- a/yaks.html
+++ b/yaks.html
@@ -36,7 +36,7 @@ <h1>Yak shaving</h1>
 
 <p>Basically my TODOs, in pursuit of the ever-pervasive <em>pleasant workflow</em>.</p>
 <section id="Replacing-tmux-with-kitty">
-<h2>Replacing tmux with kitty</h2>
+<h2>Replacing tmux with kitty<a href="#Replacing-tmux-with-kitty" class="heading-link">#</a></h2>
 <p>All blockers seem to have been addressed now?</p>
 <ul>
 <li>
@@ -48,7 +48,7 @@ <h2>Replacing tmux with kitty</h2>
 </ul>
 </section>
 <section id="Interesting-tools">
-<h2>Interesting tools</h2>
+<h2>Interesting tools<a href="#Interesting-tools" class="heading-link">#</a></h2>
 <ul>
 <li>
 <a href="https://pointlessramblings.com/posts/why-you-should-try-pyinfra/">pyinfra</a>:
@@ -70,7 +70,7 @@ <h2>Interesting tools</h2>
 </ul>
 </section>
 <section id="Home-server">
-<h2>Home server</h2>
+<h2>Home server<a href="#Home-server" class="heading-link">#</a></h2>
 <p>Specs:</p>
 <ul>
 <li>