Repos / hi.imnhan.com / ab92ff3f3d
commit ab92ff3f3db92c8c795dcc869683aa6eaa9db8e8
Author: Nhân <hi@imnhan.com>
Date:   Mon Aug 21 12:53:08 2023 +0700

    migrate remaining posts, add missing css

diff --git a/_theme/base.css b/_theme/base.css
index 813e870..c061eb7 100644
--- a/_theme/base.css
+++ b/_theme/base.css
@@ -10,7 +10,8 @@ ol {
   line-height: 1.35rem;
 }
 
-main img {
+main img,
+main video {
   max-width: 100%;
 }
 
@@ -19,3 +20,38 @@ footer {
   text-align: right;
   font-size: 0.8rem;
 }
+
+pre,
+.sidenote {
+  border: 1px solid black;
+  background-color: #eee;
+  padding: 0.5rem;
+}
+pre {
+  overflow-y: scroll;
+}
+.sidenote {
+  margin-left: 2rem;
+}
+.sidenote p {
+  margin: 0;
+}
+
+blockquote {
+  padding-left: 7px;
+  margin: 1.5rem 0 1.5rem 2rem;
+  quotes: "\201C" "\201D";
+  /* prevent long links from breaking out of the blockquote: */
+  overflow-wrap: break-word;
+}
+blockquote::before {
+  position: absolute;
+  margin-left: -0.5em;
+  margin-top: -0.25em;
+  font-size: 3em;
+  content: open-quote;
+}
+
+sup {
+  line-height: 1rem; /* prevents it from pushing up the line */
+}
diff --git a/about/index.html b/about/index.html
index fb7957d..2859fbb 100644
--- a/about/index.html
+++ b/about/index.html
@@ -35,7 +35,7 @@ <h1>About</h1>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/chromebook/index.html b/chromebook/index.html
index 5c0da84..ed57877 100644
--- a/chromebook/index.html
+++ b/chromebook/index.html
@@ -174,7 +174,7 @@ <h2>Aside: Chromebook keyboard quirks on KDE</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/cool/index.dj b/cool/index.dj
index f24ad19..5b21792 100644
--- a/cool/index.dj
+++ b/cool/index.dj
@@ -4,17 +4,15 @@ Thumb: images/sealord.png
 
 ---
 
-So recently I went to a _networking event_--something I have never liked or been good at. I'm
+So recently I went to a _networking event_---something I have never liked or been good at. I'm
 not sure if I'm one of those introverts or if I'm just socially awkward, but the very idea of
 going around trying to converse with total strangers just to exchange business cards is not at all
 appealing to me. Anyway, that's another story. Right now I want to write about something a guy
 from a non-tech company asked me:
 
-> \- Have you built anything cool?
->
-> \- [pause] Well, more or less...
->
-> \- What do you mean by "more or less"? [...] Have you built anything at all?
+> \- Have you built anything cool?\
+> \- [pause] Well, more or less...\
+> \- What do you mean by "more or less"? [...] Have you built anything at all?\
 
 Then I went on trying to explain what my recent side
 project---[pytaku](https://pytaku-legacy.appspot.com)---does and why it is
diff --git a/cool/index.html b/cool/index.html
index d002180..eadd880 100644
--- a/cool/index.html
+++ b/cool/index.html
@@ -29,15 +29,16 @@
 
 <main>
 <h1>&#34;Have you built anything cool?&#34;</h1>
-<p>So recently I went to a <em>networking event</em>–something I have never liked or been good at. I’m
+<p>So recently I went to a <em>networking event</em>—something I have never liked or been good at. I’m
 not sure if I’m one of those introverts or if I’m just socially awkward, but the very idea of
 going around trying to converse with total strangers just to exchange business cards is not at all
 appealing to me. Anyway, that’s another story. Right now I want to write about something a guy
 from a non-tech company asked me:</p>
 <blockquote>
-<p>- Have you built anything cool?</p>
-<p>- [pause] Well, more or less…</p>
-<p>- What do you mean by “more or less”? […] Have you built anything at all?</p>
+<p>- Have you built anything cool?<br>
+- [pause] Well, more or less…<br>
+- What do you mean by “more or less”? […] Have you built anything at all?<br>
+</p>
 </blockquote>
 <p>Then I went on trying to explain what my recent side
 project—<a href="https://pytaku-legacy.appspot.com">pytaku</a>—does and why it is
@@ -171,7 +172,7 @@ <h2>To sum it up…</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/custom-theme/index.html b/custom-theme/index.html
index f0f8254..d34f6a4 100644
--- a/custom-theme/index.html
+++ b/custom-theme/index.html
@@ -110,7 +110,7 @@ <h2>That’s it!</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/fcitx/index.html b/fcitx/index.html
index c7e9956..862b287 100644
--- a/fcitx/index.html
+++ b/fcitx/index.html
@@ -92,7 +92,7 @@ <h2>vim-fcitx</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/feed.xml b/feed.xml
index d8b5f96..8962e55 100644
--- a/feed.xml
+++ b/feed.xml
@@ -15,6 +15,41 @@
     <published>2023-04-22T15:55:00+07:00</published>
     <updated>2023-04-22T15:55:00+07:00</updated>
   </entry>
+  <entry>
+    <title>Go, Postgres, Caddy, systemd: a simple, highly portable, Docker-free web stack</title>
+    <id>https://hi.imnhan.com/go-stack/</id>
+    <link href="https://hi.imnhan.com/go-stack/"></link>
+    <published>2023-02-12T14:24:00+07:00</published>
+    <updated>2023-02-12T14:24:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Working with SQLite in Python without an ORM or migration framework</title>
+    <id>https://hi.imnhan.com/sqlite-python/</id>
+    <link href="https://hi.imnhan.com/sqlite-python/"></link>
+    <published>2022-01-30T14:11:00+07:00</published>
+    <updated>2022-01-30T14:11:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>I made my python webapp installable via pip</title>
+    <id>https://hi.imnhan.com/pippable-webapp/</id>
+    <link href="https://hi.imnhan.com/pippable-webapp/"></link>
+    <published>2021-10-02T19:49:00+07:00</published>
+    <updated>2021-10-02T19:49:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Opening http link under the cursor in vim</title>
+    <id>https://hi.imnhan.com/vim-open-link/</id>
+    <link href="https://hi.imnhan.com/vim-open-link/"></link>
+    <published>2021-08-07T11:37:00+07:00</published>
+    <updated>2021-08-07T11:37:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Simplest possible stepmania soft-to-hard pad mod</title>
+    <id>https://hi.imnhan.com/stepmania-pad/</id>
+    <link href="https://hi.imnhan.com/stepmania-pad/"></link>
+    <published>2021-02-08T12:47:00+07:00</published>
+    <updated>2021-02-08T12:47:00+07:00</updated>
+  </entry>
   <entry>
     <title>The video streaming finale, or why put.io is awesome</title>
     <id>https://hi.imnhan.com/video-streaming-3/</id>
@@ -22,6 +57,34 @@
     <published>2020-10-21T11:45:00+07:00</published>
     <updated>2020-10-21T11:45:00+07:00</updated>
   </entry>
+  <entry>
+    <title>Streaming videos from Google Drive: a second attempt</title>
+    <id>https://hi.imnhan.com/video-streaming-2/</id>
+    <link href="https://hi.imnhan.com/video-streaming-2/"></link>
+    <published>2020-06-10T08:25:00+07:00</published>
+    <updated>2020-06-10T08:25:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Introducing McRoss—a minimal gemini browser</title>
+    <id>https://hi.imnhan.com/mcross/</id>
+    <link href="https://hi.imnhan.com/mcross/"></link>
+    <published>2020-05-29T09:44:00+07:00</published>
+    <updated>2020-05-29T09:44:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Towards an acceptable video playing experience</title>
+    <id>https://hi.imnhan.com/video-streaming-1/</id>
+    <link href="https://hi.imnhan.com/video-streaming-1/"></link>
+    <published>2020-04-26T10:06:00+07:00</published>
+    <updated>2020-04-26T10:06:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>I did NOT sign that online petition!</title>
+    <id>https://hi.imnhan.com/petition-fraud/</id>
+    <link href="https://hi.imnhan.com/petition-fraud/"></link>
+    <published>2016-03-12T01:13:00+07:00</published>
+    <updated>2016-03-12T01:13:00+07:00</updated>
+  </entry>
   <entry>
     <title>My first DIY fightstick: Part 2</title>
     <id>https://hi.imnhan.com/fightstick-2/</id>
@@ -43,6 +106,13 @@
     <published>2015-06-05T13:54:00+07:00</published>
     <updated>2015-06-05T13:54:00+07:00</updated>
   </entry>
+  <entry>
+    <title>How to install PyQt5 on a virtualenv on Ubuntu 14.04</title>
+    <id>https://hi.imnhan.com/pyqt5/</id>
+    <link href="https://hi.imnhan.com/pyqt5/"></link>
+    <published>2015-02-14T22:33:00+07:00</published>
+    <updated>2015-02-14T22:33:00+07:00</updated>
+  </entry>
   <entry>
     <title>Dẹp ibus-unikey đi, dùng fcitx-unikey nhé!</title>
     <id>https://hi.imnhan.com/fcitx/</id>
@@ -50,6 +120,48 @@
     <published>2015-01-29T20:41:00+07:00</published>
     <updated>2015-01-29T20:41:00+07:00</updated>
   </entry>
+  <entry>
+    <title>Introducing Pytaku—the only online manga reader you&#39;ll ever need</title>
+    <id>https://hi.imnhan.com/pytaku-old/</id>
+    <link href="https://hi.imnhan.com/pytaku-old/"></link>
+    <published>2015-01-02T21:19:00+07:00</published>
+    <updated>2015-01-02T21:19:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Virtualenv(wrapper), python2 and python3</title>
+    <id>https://hi.imnhan.com/virtualenvwrapper/</id>
+    <link href="https://hi.imnhan.com/virtualenvwrapper/"></link>
+    <published>2014-12-16T21:35:00+07:00</published>
+    <updated>2014-12-16T21:35:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Enable italic text inside vim inside tmux inside gnome-terminal</title>
+    <id>https://hi.imnhan.com/tmux-italics/</id>
+    <link href="https://hi.imnhan.com/tmux-italics/"></link>
+    <published>2014-08-02T16:46:00+07:00</published>
+    <updated>2014-08-02T16:46:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Setting up your development environment for a node-webkit project</title>
+    <id>https://hi.imnhan.com/node-webkit/</id>
+    <link href="https://hi.imnhan.com/node-webkit/"></link>
+    <published>2014-05-01T08:23:00+07:00</published>
+    <updated>2014-05-01T08:23:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>How I bypassed my university&#39;s domain blocker to watch movies on hdviet.com</title>
+    <id>https://hi.imnhan.com/hdviet/</id>
+    <link href="https://hi.imnhan.com/hdviet/"></link>
+    <published>2014-03-17T21:58:00+07:00</published>
+    <updated>2014-03-17T21:58:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>What I did after installing Manjaro xfce</title>
+    <id>https://hi.imnhan.com/manjaro-xfce/</id>
+    <link href="https://hi.imnhan.com/manjaro-xfce/"></link>
+    <published>2014-02-04T20:20:13+07:00</published>
+    <updated>2014-02-04T20:20:13+07:00</updated>
+  </entry>
   <entry>
     <title>&#34;Have you built anything cool?&#34;</title>
     <id>https://hi.imnhan.com/cool/</id>
@@ -57,4 +169,32 @@
     <published>2014-01-25T10:37:00+07:00</published>
     <updated>2014-01-25T10:37:00+07:00</updated>
   </entry>
+  <entry>
+    <title>Installing programs in Ubuntu</title>
+    <id>https://hi.imnhan.com/ubuntu-programs/</id>
+    <link href="https://hi.imnhan.com/ubuntu-programs/"></link>
+    <published>2013-09-06T21:03:00+07:00</published>
+    <updated>2013-09-06T21:03:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Fix RMIT wi-fi issue in Ubuntu 13.04 and variants</title>
+    <id>https://hi.imnhan.com/rmit-wifi/</id>
+    <link href="https://hi.imnhan.com/rmit-wifi/"></link>
+    <published>2013-06-17T08:12:00+07:00</published>
+    <updated>2013-06-17T08:12:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Why I use Linux: Automation</title>
+    <id>https://hi.imnhan.com/linux-automation/</id>
+    <link href="https://hi.imnhan.com/linux-automation/"></link>
+    <published>2013-06-07T08:02:00+07:00</published>
+    <updated>2013-06-07T08:02:00+07:00</updated>
+  </entry>
+  <entry>
+    <title>Modern vim plugin management: Pathogen vs Vundle</title>
+    <id>https://hi.imnhan.com/pathogen-vs-vundle/</id>
+    <link href="https://hi.imnhan.com/pathogen-vs-vundle/"></link>
+    <published>2013-05-13T12:00:00+07:00</published>
+    <updated>2013-05-13T12:00:00+07:00</updated>
+  </entry>
 </feed>
\ No newline at end of file
diff --git a/fightstick-1/index.html b/fightstick-1/index.html
index 027368c..9eb1729 100644
--- a/fightstick-1/index.html
+++ b/fightstick-1/index.html
@@ -201,7 +201,7 @@ <h2>Um… that’s it (for now).</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/fightstick-2/index.html b/fightstick-2/index.html
index c0e2f98..9314d18 100644
--- a/fightstick-2/index.html
+++ b/fightstick-2/index.html
@@ -84,7 +84,7 @@ <h2>Thoughts</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/go-stack/index.dj b/go-stack/index.dj
index a3ba04c..f344a91 100644
--- a/go-stack/index.dj
+++ b/go-stack/index.dj
@@ -1,6 +1,6 @@
 Title: Go, Postgres, Caddy, systemd: a simple, highly portable, Docker-free web stack
-Slug: go-postgres-caddy-systemd-stack
-Date: 2023-02-12 14:24
+PostedAt: 2023-02-12 14:24
+---
 
 I've [mentioned][1] before that I'm not a fan of Docker as a deployment
 strategy. In that same post I briefly mentioned that Go could simplify
@@ -27,8 +27,8 @@ that boilerplate for reading data into Go structs though. I heard good things
 about [sqlc][5], which generates Go code from SQL queries. I'll most likely try
 that next.
 
-Sticking to pure Go code brings 2 big benefits: **independence from glibc** and
-**effortless cross-compilation**.
+Sticking to pure Go code brings 2 big benefits: *independence from glibc* and
+*effortless cross-compilation*.
 
 Not depending on glibc means our compiled executable for, say, Linux, will run,
 not only on any Linux distro regardless of its glibc version, but also on
@@ -247,16 +247,16 @@ before][1], and I'll say it again:
 > simple to deploy regardless of whether you're using docker, packer, ansible,
 > pyinfra, podman, nomad, k8s, k3s, an impenetrable shell script some dude
 > wrote 2 years ago who just left the company last month... or any combination
-> of the above. The point is **you shouldn't be forced to use more heavyweight
+> of the above. The point is *you shouldn't be forced to use more heavyweight
 > solutions just because the software is a pain in the butt to setup
-> manually**.
+> manually*.
 
 Sooner or later we'll all have to peek under the hood to diagnose problems, and
 the fewer moving pieces you have to learn and understand, the more grateful
 you'll be to your predecessors (and, let's be honest, the fewer profanities
 you'll have to utter to yourself).
 
-[1]: /posts/i-made-my-python-webapp-pip-installable/
+[1]: ../pippable-webapp/
 [2]: https://lets-go.alexedwards.net/
 [3]: https://www.alexedwards.net/blog/introducing-flow
 [4]: https://github.com/lib/pq
diff --git a/go-stack/index.html b/go-stack/index.html
new file mode 100644
index 0000000..06e7768
--- /dev/null
+++ b/go-stack/index.html
@@ -0,0 +1,271 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Go, Postgres, Caddy, systemd: a simple, highly portable, Docker-free web stack | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2023-02-12">
+        Sunday, 12 Feb 2023
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Go, Postgres, Caddy, systemd: a simple, highly portable, Docker-free web stack</h1>
+<p>I’ve <a href="../pippable-webapp/">mentioned</a> before that I’m not a fan of Docker as a deployment
+strategy. In that same post I briefly mentioned that Go could simplify
+deployment compared to Python. Today I’ll <em>go</em> (haha get it?) into detail,
+warts and all, how I recently set up a publicly accessible Go web service,
+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>
+<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,
+it’s cumbersome to define routes using only <code>net/http</code>, so I recommend using
+the very minimal <a href="https://www.alexedwards.net/blog/introducing-flow">flow</a> routing library written by the same author: it
+offers a cleaner API while having very little code itself. Heck, you should
+probably vendor it and later customize whichever way you want.</p>
+<p>As for the PostgreSQL driver, I chose <a href="https://github.com/lib/pq">github.com/lib/pq</a> simply because
+it’s a pure Go library that implements the standard <code>database/sql</code> interface.
+I preferred to learn the most common API before branching into more
+special-purpose stuff. It quickly became tedious and error-prone to write all
+that boilerplate for reading data into Go structs though. I heard good things
+about <a href="https://sqlc.dev/">sqlc</a>, which generates Go code from SQL queries. I’ll most likely try
+that next.</p>
+<p>Sticking to pure Go code brings 2 big benefits: <strong>independence from glibc</strong> and
+<strong>effortless cross-compilation</strong>.</p>
+<p>Not depending on glibc means our compiled executable for, say, Linux, will run,
+not only on any Linux distro regardless of its glibc version, but also on
+distros that use alternative libc implementations e.g. musl. Coming from
+Python, it’s incredibly liberating to no longer have to find an ancient distro
+with the oldest possible glibc to build my executables on (most Python projects
+that do anything useful use C extensions, sadly). It’s not without caveat
+though: some of Go’s own standard libraries, namely <code>net</code> and <code>os/user</code>, use cgo
+by default. We can set <code>CGO_ENABLED=0</code> to avoid that, which tells the Go
+compiler to use their alternative pure Go implementations, but those are not as
+full-featured. If your code or dependency requires those, make sure to check if
+they work correctly with the pure Go version. The easiet way to confirm that
+your compiled executable is truly static is using either <code>ldd</code> or <code>file</code>:</p>
+<pre><code class="language-sh">$ ldd mybinary
+#        not a dynamic executable
+
+$ file mybinary | tr , '\n'
+# mybinary: ELF 64-bit LSB executable
+#  x86-64
+#  version 1 (SYSV)
+#  statically linked
+#  Go BuildID=[...]
+#  with debug_info
+#  not stripped
+</code></pre>
+<p>Cross-compilation is self-explanatory: out of the box, you can compile to any
+architecture/OS combination that Go supports. No more looking for the right CI
+service or docker container to build your stuff in.</p>
+<p>See also:</p>
+<ul>
+<li>
+<a href="https://dave.cheney.net/2016/01/18/cgo-is-not-go">cgo is not Go</a>: a more exhaustive argument for staying in pure
+Go-land.
+</li>
+<li>
+<a href="https://www.arp242.net/static-go.html">Statically compile Go programs</a>: a deep dive into static Go compilation
+and its quirks, complete with examples on how to statically link against
+SQLite with musl libc, if you must.
+</li>
+</ul>
+</section>
+<section id="Postgres">
+<h2>Postgres</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>
+<p>Postgres has none of those quirks, but causes extra operational complexity, not
+only for deployment, but also for development: you now need to erect a Postgres
+server with the right db/user/password combination for each project.</p>
+<p>From the local development perspective, this is actually one of the few cases
+where Docker rightfully shines: whip up a tiny docker-compose.yml, hit that
+<code>docker-compose up</code> command, and you’ve got yourself a nicely isolated,
+delightfully disposable postgres server with your desired user/password/db
+combination, exposed at the exact port you want:</p>
+<pre><code class="language-yaml"># docker-compose.yml
+version: '3.9'
+services:
+  db:
+    image: postgres:15
+    ports: 127.0.0.1:5432:5432
+    environment:
+      POSTGRES_USER: example
+      POSTGRES_PASSWORD: example
+      POSTGRES_DB: example
+</code></pre>
+<p>Since a developer’s computer is typically not lacking in resources, we can get
+away with docker’s storage overhead, and, in MacOS’s case, VM overhead.</p>
+<p>But what if we want to stick to our anti-docker guns? Good news: it’s still
+possible to have <a href="https://jamey.thesharps.us/2019/05/29/per-project-postgres/">Per-project Postgres</a> instances. Here’s the gist:</p>
+<pre><code class="language-sh">cd my-project
+mkdir .pgres # postgres data dir
+
+# These envars tell postgres cli tools to:
+# a) put data files in .pgres
+# b) connect to server via a socket inside .pgres
+export PGDATA="$PWD/.pgres"
+export PGHOST="$PWD/.pgres"
+
+initdb # populate .pgres/
+
+echo "listen_addresses = ''" &gt;&gt; .pgres/postgresql.conf
+echo "unix_socket_directories = '$PGHOST'" &gt;&gt; .pgres/postgresql.conf
+
+echo "CREATE DATABASE $USER;" | postgres --single -E postgres
+</code></pre>
+<p><em>(I also made a <a href="https://github.com/nhanb/neodots/blob/f79713b4e79c5da4fa92f75b1537b73b4c114d03/fish/scripts/standalone-postgres">python script</a> to automate this process)</em></p>
+<p>Now whenever you develop on this project, just cd into the project dir, make
+sure $PGDATA and $PGHOST point to the correct dir, then run <code>postgres</code>. You can
+save those environment variables into a <code>setenv.sh</code> script to source every
+time, or use tools like <a href="https://direnv.net/">direnv</a> to automatically set them on cd. When you
+no longer need it, cleaning up is as simple as removing the .pgres dir.</p>
+<p>On the server side, if you’re on, say, Debian, the Postgres developers maintain
+an <a href="https://www.postgresql.org/download/linux/debian/">Apt repo</a> that provides any currently supported version of Postgres, so
+you can always use the latest and greatest DB while still enjoying the
+stability of Debian. Just follow the instructions to add the repo, install your
+preferred postgres version, then enable &amp; start the postgresql service using
+<code>systemctl</code>.</p>
+<p>You’ll then need to follow the distro’s <a href="https://wiki.debian.org/PostgreSql">convention</a> to create a DB with
+its dedicated username/password combination. Here’s how I set up mine:</p>
+<pre><code class="language-sh">$ su - postgres
+(as postgres) $ createuser --pwprompt mypguser
+(as postgres) $ createdb -O mypguser mypgdatabase
+</code></pre>
+<p>I didn’t bother to create a dedicated OS user, because I’ll later use systemd’s
+DynamicUser feature to run my service on its own dynamically created user
+anyway. This brings us to…</p>
+</section>
+<section id="systemd">
+<h2>systemd</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.
+(Docker Compose would kinda work too, but we’re trying to see if we can avoid
+gratuitous container usage here, remember?)</p>
+<p>Nowadays though, systemd is already pervasive in mainstream Linux distros, and
+comes tightly integrated with supporting services e.g. journald, so it makes
+little sense to use anything else for service management.</p>
+<p>To limit the blast radius if (when?) a service gets pwn’ed, it’s recommended to
+run each service as its own OS user that only has access to what it actually
+needs. In the past I used to create 1 system user to run each service as, but
+this time I realized I could use systemd’s <a href="https://0pointer.net/blog/dynamic-users-with-systemd.html">DynamicUser</a> instead:</p>
+<pre><code class="language-ini"># /etc/systemd/system/myservice.service
+[Service]
+ExecStart=/usr/local/bin/myservice
+Environment=MYSERVICE_DB=postgres://db-user:db-password@localhost/db-name
+Environment=MYSERVICE_ADDR=localhost:8000
+DynamicUser=1
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+</code></pre>
+<p>It’s just a little less work compared to creating a system user with the
+correct restrictions and running the service under that user, but hey, less
+work is less work! Also that’s one fewer thing that I have to worry about
+messing up.</p>
+<p>You may have noticed the <code>ExecStart=/usr/local/bin/myservice</code> line, which
+assumes my service’s executable is in /usr/local/bin/. Since my service is only
+1 binary with no support files, this, and postgres credentials (provided via
+the <code>MYSERVICE_DB</code> envar), are all that’s needed to run the service. It also
+means for subsequent deployments, this will be my entire deployment procedure:</p>
+<pre><code class="language-sh"># compile:
+CGO_ENABLED=0 go build -o dist/myservice
+# copy binary to server (scp works too):
+rsync -av dist/myservice myserver:/usr/local/bin/myservice
+# restart service:
+ssh myserver systemctl restart myservice
+</code></pre>
+</section>
+<section id="Caddy">
+<h2>Caddy</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
+2 lines of config to:</p>
+<ul>
+<li>
+Serve HTTPS at port 443, with a valid cert provided by Let’s Encrypt, using
+reasonable default cryptographic settings—I just ran my site through the
+<a href="https://www.ssllabs.com/ssltest/">ssllabs.com test</a> and it handily scored an A.
+</li>
+<li>
+Serve HTTP at port 80 that simply redirects to the HTTPS port
+</li>
+</ul>
+<pre><code class="language-nginx"># /etc/caddy/Caddyfile
+my-domain.com {
+    reverse_proxy localhost:8000
+}
+</code></pre>
+<p>There are many interesting problems to solve when running a web service, and
+HTTPS cert bookkeeping is not one of them, so I’m more than happy to stop
+fiddling with certbot cron jobs.</p>
+</section>
+<section id="Closing-remarks">
+<h2>Closing remarks</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
+upgrades is also a good idea. But of course these depend heavily on each
+person’s requirements and tastes.</p>
+<p>Of course I’m not advocating for manual “pet” server maintenance everywhere.
+Nothing from this setup prevents you from doing proper automated provisioning,
+configuration management, so on and so forth. In fact, it is easier to e.g.
+write an ansible playbook for this setup, because it’s simpler: you don’t have
+to worry about setting up the correct python virtual environment, or making
+nginx and certbot play well with each other. Hell, you can dockerize parts of
+this setup, and your Dockerfiles will be simpler thanks to it. I’ve <a href="../pippable-webapp/">said it
+before</a>, and I’ll say it again:</p>
+<blockquote>
+<p>Throwing abstractions over complex procedures is simply shifting the costs
+elsewhere. Shipping your software in a Dockerfile is fine, but making your
+distribution so simple that people can easily write a couple of lines of
+Dockerfile for it by themselves is more valuable. Simple distribution is
+simple to deploy regardless of whether you’re using docker, packer, ansible,
+pyinfra, podman, nomad, k8s, k3s, an impenetrable shell script some dude
+wrote 2 years ago who just left the company last month… or any combination
+of the above. The point is <strong>you shouldn’t be forced to use more heavyweight
+solutions just because the software is a pain in the butt to setup
+manually</strong>.</p>
+</blockquote>
+<p>Sooner or later we’ll all have to peek under the hood to diagnose problems, and
+the fewer moving pieces you have to learn and understand, the more grateful
+you’ll be to your predecessors (and, let’s be honest, the fewer profanities
+you’ll have to utter to yourself).</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/hdviet_01_forbidden.png b/hdviet/hdviet_01_forbidden.png
similarity index 100%
rename from content/images/hdviet_01_forbidden.png
rename to hdviet/hdviet_01_forbidden.png
diff --git a/content/images/hdviet_02_forbidden_direct.png b/hdviet/hdviet_02_forbidden_direct.png
similarity index 100%
rename from content/images/hdviet_02_forbidden_direct.png
rename to hdviet/hdviet_02_forbidden_direct.png
diff --git a/content/images/hdviet_03_ip.png b/hdviet/hdviet_03_ip.png
similarity index 100%
rename from content/images/hdviet_03_ip.png
rename to hdviet/hdviet_03_ip.png
diff --git a/content/images/hdviet_04_firefox_proxy.png b/hdviet/hdviet_04_firefox_proxy.png
similarity index 100%
rename from content/images/hdviet_04_firefox_proxy.png
rename to hdviet/hdviet_04_firefox_proxy.png
diff --git a/content/images/hdviet_05_no_sub.png b/hdviet/hdviet_05_no_sub.png
similarity index 100%
rename from content/images/hdviet_05_no_sub.png
rename to hdviet/hdviet_05_no_sub.png
diff --git a/content/images/hdviet_06_404.png b/hdviet/hdviet_06_404.png
similarity index 100%
rename from content/images/hdviet_06_404.png
rename to hdviet/hdviet_06_404.png
diff --git a/content/images/hdviet_07_graph.png b/hdviet/hdviet_07_graph.png
similarity index 100%
rename from content/images/hdviet_07_graph.png
rename to hdviet/hdviet_07_graph.png
diff --git a/content/images/hdviet_08_srt.png b/hdviet/hdviet_08_srt.png
similarity index 100%
rename from content/images/hdviet_08_srt.png
rename to hdviet/hdviet_08_srt.png
diff --git a/hdviet/index.dj b/hdviet/index.dj
index 28543c6..db66176 100644
--- a/hdviet/index.dj
+++ b/hdviet/index.dj
@@ -1,19 +1,16 @@
 Title: How I bypassed my university's domain blocker to watch movies on hdviet.com
-Date: 2014-03-17 21:58
-Category: tutorials
-Tags: ubuntu, linux
-Slug: how-i-bypassed-my-university-domain-blocker-to-access-hdviet
+PostedAt: 2014-03-17 21:58
 Thumb: images/hdviet_05_no_sub.png
+---
 
-
-**TL;DR**: Clone [my script from GitHub][4], run it with `python2 server.py 8080`, configure your
+*TL;DR*: Clone [my script from GitHub][4], run it with `python2 server.py 8080`, configure your
 browser to use localhost:8080 as HTTP and HTTPS proxy, profit.
 
-**Disclaimer**: The sole reason I came up with this trick and documented it was to satisfy my
+*Disclaimer*: The sole reason I came up with this trick and documented it was to satisfy my
 curiosity. I don't come to campus often anymore so it's not like I'm going to spend 8 hours a day
 wasting the university's internet bandwidth for "Two and a half men" anyway...
 
-**Another Note** (last one, promise!): If you're using Mac OS X or Windows, Proxifier will probably
+*Another Note* (last one, promise!): If you're using Mac OS X or Windows, Proxifier will probably
 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!
 
@@ -28,11 +25,11 @@ Hdviet's case is a bit special: the domain `hdviet.com` itself is not blocked, b
 the actual server hosting its playlists & videos, `v-01.vn-hd.com`, is. A quick look at Firefox's
 excellent Network inspector confirmed that:
 
-![](/images/hdviet_01_forbidden.png)
+![](hdviet_01_forbidden.png)
 
 If you request the file directly:
 
-![](/images/hdviet_02_forbidden_direct.png)
+![](hdviet_02_forbidden_direct.png)
 
 ## Going for the IP
 
@@ -40,7 +37,7 @@ Naturally, I wanted to check if I could access the resource directly via the IP.
 up a domain's IP is using [ping.eu][1]. Once you've got the IP, try replacing the domain with it in
 the failed request:
 
-![](/images/hdviet_03_ip.png)
+![](hdviet_03_ip.png)
 
 This time it works, which means only the domain is blocked, not the IP.
 
@@ -60,24 +57,27 @@ HTTP proxy library.
 
 To install twisted, use `pip`:
 
-    :::bash
-    sudo pip install twisted
+```sh
+sudo pip install twisted
+```
 
 Since the default implementation doesn't support HTTPS, we'll use a [powered-up one][3] I found on
 GitHub, written by Peter Ruibal. Let's clone this thing:
 
-    :::bash
-    git clone https://github.com/fmoo/twisted-connect-proxy.git
+```sh
+git clone https://github.com/fmoo/twisted-connect-proxy.git
+```
 
 Now let's try running the proxy server: `cd` into the cloned directory and run it with `python2`:
 
-    :::bash
-    cd twisted-connect-proxy
-    python2 server.py 8080
+```sh
+cd twisted-connect-proxy
+python2 server.py 8080
+```
 
-Then configure your browser to use **localhost:8080** as the proxy. For Firefox it's easy:
+Then configure your browser to use `localhost:8080` as the proxy. For Firefox it's easy:
 
-![](/images/hdviet_04_firefox_proxy.png)
+![](hdviet_04_firefox_proxy.png)
 
 You should now be able to surf the web through the running proxy. But hey, you still can't visit
 any blocked site! Of course you can't, since we haven't replaced the domains with IPs. Let's do
@@ -87,54 +87,57 @@ that.
 
 Open `server.py`, look for this part:
 
-    :::python
-    class ConnectProxyRequest(ProxyRequest):
-        """HTTP ProxyRequest handler (factory) that supports CONNECT"""
-        connectedProtocol = None
+```python
+class ConnectProxyRequest(ProxyRequest):
+    """HTTP ProxyRequest handler (factory) that supports CONNECT"""
+    connectedProtocol = None
 
-        def process(self):
-            if self.method == 'CONNECT':
-                self.processConnectRequest()
-            else:
-                ProxyRequest.process(self)
+    def process(self):
+        if self.method == 'CONNECT':
+            self.processConnectRequest()
+        else:
+            ProxyRequest.process(self)
+```
 
 The `process()` method is in charge of forwarding whatever request the proxy receives to the actual
 target server. Let's intercept it with our own `redirect()` function:
 
-    :::python
-    redirects = {
-        'v-01.vn-hd.com': '125.212.216.93',  # video
-        's.vn-hd.com': '210.211.120.146',  # sub
-    }
-
-    def redirect(req):
-        for domain, ip in redirects.items():
-            if req.path.find(domain) != -1:  # check if we're requesting a blocked domain
-                req.uri = req.uri.replace(domain, ip, 1)
-                req.path = req.path.replace(domain, ip, 1)
-                req.requestHeaders.setRawHeaders('host', [ip])  # replace "Host" header too
-                return
-
-    class ConnectProxyRequest(ProxyRequest):
-        """HTTP ProxyRequest handler (factory) that supports CONNECT"""
-        connectedProtocol = None
-
-        def process(self):
-            redirect(self)  # intercept request processing
-            if self.method == 'CONNECT':
-                self.processConnectRequest()
-        # the rest of the file ...
+```python
+redirects = {
+    'v-01.vn-hd.com': '125.212.216.93',  # video
+    's.vn-hd.com': '210.211.120.146',  # sub
+}
+
+def redirect(req):
+    for domain, ip in redirects.items():
+        if req.path.find(domain) != -1:  # check if we're requesting a blocked domain
+            req.uri = req.uri.replace(domain, ip, 1)
+            req.path = req.path.replace(domain, ip, 1)
+            req.requestHeaders.setRawHeaders('host', [ip])  # replace "Host" header too
+            return
+
+class ConnectProxyRequest(ProxyRequest):
+    """HTTP ProxyRequest handler (factory) that supports CONNECT"""
+    connectedProtocol = None
+
+    def process(self):
+        redirect(self)  # intercept request processing
+        if self.method == 'CONNECT':
+            self.processConnectRequest()
+    # the rest of the file ...
+```
 
 In the snippet above, we defined a dictionary `redirects` that stores the blocked domains that we
-need to replace. Note that I added **s.vn-hd.com** as well, which is the host that stores
+need to replace. Note that I added `s.vn-hd.com` as well, which is the host that stores
 subtitles. In our actual `redirect()` function, we check if the request being processed is pointing
 to any of the blocked domains defined earlier, then replace domain with its corresponding IP if
 there is a match:
 
-    :::python
-    req.uri = req.uri.replace(domain, ip, 1)
-    req.path = req.path.replace(domain, ip, 1)
-    req.requestHeaders.setRawHeaders('host', [ip])
+```python
+req.uri = req.uri.replace(domain, ip, 1)
+req.path = req.path.replace(domain, ip, 1)
+req.requestHeaders.setRawHeaders('host', [ip])
+```
 
 Note that the 3rd line also changes the "Host" HTTP header. Yes, our beloved people from IT
 Services do inspect HTTP headers to block stuff too. This line will introduce another problem that
@@ -143,31 +146,35 @@ I will explain later in this post.
 Now restart our proxy server and check the link again. It should work. You can now watch stuff, but
 you'll notice that English subtitles are not shown even if you turn them on:
 
-![](/images/hdviet_05_no_sub.png)
+![](hdviet_05_no_sub.png)
 
 If you open the browser's network inspector, reload the page and try to enable English subtitles
 again, you'll see the problem:
 
-![](/images/hdviet_06_404.png)
+![](hdviet_06_404.png)
 
 The link in question is:
 
-    :::text
-    http://s.vn-hd.com/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+```
+http://s.vn-hd.com/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+```
 
-Since **s.vn-hd.com** is in our blocked domain dictionary (`redirects`), the proxy server will
+Since `s.vn-hd.com` is in our blocked domain dictionary (`redirects`), the proxy server will
 request this:
 
-    :::text
-    http://210.211.120.146/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+```
+http://210.211.120.146/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+```
 
 If you try to open it directly in a browser (that isn't using our proxy server), you'll get a 404
-too.  Why is that? This is because the **Host** header is also changed to **210.211.120.146**
-instead of the original domain **s.vn-hd.com**. Normally a single web server can be serving
+too.  Why is that? This is because the `Host` header is also changed to
+`210.211.120.146`
+instead of the original domain `s.vn-hd.com`. Normally a single web server can be serving
 multiple domains at a time, and when we send an HTTP request, we need to specify `Host: <domain>`
-for the server to know which domain we want to get the resource from. When the **Host** header is
+for the server to know which domain we want to get the resource from. When the
+`Host` header is
 simply the IP, the server may get confused and therefore cannot serve the correct resource. As for
-**v-01.vn-hd.com**, we got lucky in that case.
+`v-01.vn-hd.com`, we got lucky in that case.
 
 On the other hand, if we keep `Host: s.vn-hd.com` as-is, RMIT will be able to block our request.
 This leads to our final trick:
@@ -177,71 +184,75 @@ This leads to our final trick:
 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
-App Engine website at **hdviet-proxy.appspot.com**. It works like this:
-
-![](/images/hdviet_07_graph.png)
-
-Now we need to edit our server code to redirect any **s.vn-hd.com** request to
-**hdviet-proxy.appspot.com/?url=original_url**.
-
-    :::python
-    import urllib
-
-    sub_server = 's.vn-hd.com'
-    remote_server = 'hdviet-proxy.appspot.com'
-    redirects = {
-        'v-01.vn-hd.com': '125.212.216.93',  # video
-    }
-
-    def redirect(req):
-        for domain, ip in redirects.items():
-            if req.path.find(domain) != -1:
-                req.uri = req.uri.replace(domain, ip, 1)
-                req.path = req.path.replace(domain, ip, 1)
-                req.requestHeaders.setRawHeaders('host', [ip])
-                return
-            elif req.path.find(sub_server) != -1:
-                proxied_url = 'http://%s/?%s' % (remote_server,
-                                                 urllib.urlencode({'url': req.uri}))
-                req.uri = proxied_url
-                req.path = req.path.replace(sub_server, remote_server)
-                req.requestHeaders.setRawHeaders('host', [remote_server])
-                return
+App Engine website at `hdviet-proxy.appspot.com`. It works like this:
+
+![](hdviet_07_graph.png)
+
+Now we need to edit our server code to redirect any `s.vn-hd.com` request to
+`hdviet-proxy.appspot.com/?url=original_url`.
+
+```python
+import urllib
+
+sub_server = 's.vn-hd.com'
+remote_server = 'hdviet-proxy.appspot.com'
+redirects = {
+    'v-01.vn-hd.com': '125.212.216.93',  # video
+}
+
+def redirect(req):
+    for domain, ip in redirects.items():
+        if req.path.find(domain) != -1:
+            req.uri = req.uri.replace(domain, ip, 1)
+            req.path = req.path.replace(domain, ip, 1)
+            req.requestHeaders.setRawHeaders('host', [ip])
+            return
+        elif req.path.find(sub_server) != -1:
+            proxied_url = 'http://%s/?%s' % (remote_server,
+                                                urllib.urlencode({'url': req.uri}))
+            req.uri = proxied_url
+            req.path = req.path.replace(sub_server, remote_server)
+            req.requestHeaders.setRawHeaders('host', [remote_server])
+            return
+```
 
 You can view my [finished script on github][4] and clone it to use right away.
 
 If you want to set up your own website instead of using mine, it's really simple. Just use the new
 site template provided with GAE SDK and edit `main.py` like so:
 
-    :::python
-    import webapp2
-    from google.appengine.api import urlfetch
+```python
+import webapp2
+from google.appengine.api import urlfetch
 
-    class MainHandler(webapp2.RequestHandler):
-        def get(self):
-            url = self.request.get('url')
-            resp = urlfetch.fetch(url).content
-            self.response.write(resp)
+class MainHandler(webapp2.RequestHandler):
+    def get(self):
+        url = self.request.get('url')
+        resp = urlfetch.fetch(url).content
+        self.response.write(resp)
 
-    app = webapp2.WSGIApplication([
-        ('/', MainHandler)
-    ], debug=True)
+app = webapp2.WSGIApplication([
+    ('/', MainHandler)
+], debug=True)
+```
 
 Remember to change the `remote_server` variable in `server.py` to match your appspot link.
 
 Restart the server script, now when the browser requests for this:
 
-    :::text
-    http://s.vn-hd.com/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+```
+http://s.vn-hd.com/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+```
 
 `server.py` will redirect to this:
 
-    :::text
-    http://hdviet-proxy.appspot.com/?url=http%3A%2F%2Fs.vn-hd.com%2Fstore6%2F21042013%2FTwo_and_a_Half_Men_S02%2FE001%2FTwo_and_a_Half_Men_S02_E001_ENG.srt
+```
+http://hdviet-proxy.appspot.com/?url=http%3A%2F%2Fs.vn-hd.com%2Fstore6%2F21042013%2FTwo_and_a_Half_Men_S02%2FE001%2FTwo_and_a_Half_Men_S02_E001_ENG.srt
+```
 
 And the appspot site will get the original url, fetch its content, and give it right back to us:
 
-![](/images/hdviet_08_srt.png)
+![](hdviet_08_srt.png)
 
 You should now be able to watch movies with subtitles. Congratulations!
 
diff --git a/hdviet/index.html b/hdviet/index.html
new file mode 100644
index 0000000..e5dafbe
--- /dev/null
+++ b/hdviet/index.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>How I bypassed my university&#39;s domain blocker to watch movies on hdviet.com | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2014-03-17">
+        Monday, 17 Mar 2014
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>How I bypassed my university&#39;s domain blocker to watch movies on hdviet.com</h1>
+<p><strong>TL;DR</strong>: Clone <a href="https://github.com/nhanb/twisted-connect-proxy">my script from GitHub</a>, run it with <code>python2 server.py 8080</code>, configure your
+browser to use localhost:8080 as HTTP and HTTPS proxy, profit.</p>
+<p><strong>Disclaimer</strong>: The sole reason I came up with this trick and documented it was to satisfy my
+curiosity. I don’t come to campus often anymore so it’s not like I’m going to spend 8 hours a day
+wasting the university’s internet bandwidth for “Two and a half men” anyway…</p>
+<p><strong>Another Note</strong> (last one, promise!): If you’re using Mac OS X or Windows, Proxifier will probably
+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>
+<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
+can’t go to certain blacklisted websites. (mediafire, fshare, gamevn, vnsharing, etc.)</p>
+<p>Hdviet’s case is a bit special: the domain <code>hdviet.com</code> itself is not blocked, but the domain of
+the actual server hosting its playlists &amp; videos, <code>v-01.vn-hd.com</code>, is. A quick look at Firefox’s
+excellent Network inspector confirmed that:</p>
+<p><img alt="" src="hdviet_01_forbidden.png"></p>
+<p>If you request the file directly:</p>
+<p><img alt="" src="hdviet_02_forbidden_direct.png"></p>
+</section>
+<section id="Going-for-the-IP">
+<h2>Going for the IP</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>
+<p><img alt="" src="hdviet_03_ip.png"></p>
+<p>This time it works, which means only the domain is blocked, not the IP.</p>
+<p>One thing worth noting about hdviet: The video is not served as 1 single file, it is instead
+chopped into multiple parts, which are loaded in order. Therefore, our first job is to
+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>
+<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
+“battery-included” they mean that most of the common functionalities have already been implemented
+so we can use them out of the box. For the purpose of this tutorial, we are only interested in its
+HTTP proxy library.</p>
+<p>To install twisted, use <code>pip</code>:</p>
+<pre><code class="language-sh">sudo pip install twisted
+</code></pre>
+<p>Since the default implementation doesn’t support HTTPS, we’ll use a <a href="https://github.com/fmoo/twisted-connect-proxy">powered-up one</a> I found on
+GitHub, written by Peter Ruibal. Let’s clone this thing:</p>
+<pre><code class="language-sh">git clone https://github.com/fmoo/twisted-connect-proxy.git
+</code></pre>
+<p>Now let’s try running the proxy server: <code>cd</code> into the cloned directory and run it with <code>python2</code>:</p>
+<pre><code class="language-sh">cd twisted-connect-proxy
+python2 server.py 8080
+</code></pre>
+<p>Then configure your browser to use <code>localhost:8080</code> as the proxy. For Firefox it’s easy:</p>
+<p><img alt="" src="hdviet_04_firefox_proxy.png"></p>
+<p>You should now be able to surf the web through the running proxy. But hey, you still can’t visit
+any blocked site! Of course you can’t, since we haven’t replaced the domains with IPs. Let’s do
+that.</p>
+</section>
+<section id="Domain-to-IP">
+<h2>Domain to IP</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"""
+    connectedProtocol = None
+
+    def process(self):
+        if self.method == 'CONNECT':
+            self.processConnectRequest()
+        else:
+            ProxyRequest.process(self)
+</code></pre>
+<p>The <code>process()</code> method is in charge of forwarding whatever request the proxy receives to the actual
+target server. Let’s intercept it with our own <code>redirect()</code> function:</p>
+<pre><code class="language-python">redirects = {
+    'v-01.vn-hd.com': '125.212.216.93',  # video
+    's.vn-hd.com': '210.211.120.146',  # sub
+}
+
+def redirect(req):
+    for domain, ip in redirects.items():
+        if req.path.find(domain) != -1:  # check if we're requesting a blocked domain
+            req.uri = req.uri.replace(domain, ip, 1)
+            req.path = req.path.replace(domain, ip, 1)
+            req.requestHeaders.setRawHeaders('host', [ip])  # replace "Host" header too
+            return
+
+class ConnectProxyRequest(ProxyRequest):
+    """HTTP ProxyRequest handler (factory) that supports CONNECT"""
+    connectedProtocol = None
+
+    def process(self):
+        redirect(self)  # intercept request processing
+        if self.method == 'CONNECT':
+            self.processConnectRequest()
+    # the rest of the file ...
+</code></pre>
+<p>In the snippet above, we defined a dictionary <code>redirects</code> that stores the blocked domains that we
+need to replace. Note that I added <code>s.vn-hd.com</code> as well, which is the host that stores
+subtitles. In our actual <code>redirect()</code> function, we check if the request being processed is pointing
+to any of the blocked domains defined earlier, then replace domain with its corresponding IP if
+there is a match:</p>
+<pre><code class="language-python">req.uri = req.uri.replace(domain, ip, 1)
+req.path = req.path.replace(domain, ip, 1)
+req.requestHeaders.setRawHeaders('host', [ip])
+</code></pre>
+<p>Note that the 3rd line also changes the “Host” HTTP header. Yes, our beloved people from IT
+Services do inspect HTTP headers to block stuff too. This line will introduce another problem that
+I will explain later in this post.</p>
+<p>Now restart our proxy server and check the link again. It should work. You can now watch stuff, but
+you’ll notice that English subtitles are not shown even if you turn them on:</p>
+<p><img alt="" src="hdviet_05_no_sub.png"></p>
+<p>If you open the browser’s network inspector, reload the page and try to enable English subtitles
+again, you’ll see the problem:</p>
+<p><img alt="" src="hdviet_06_404.png"></p>
+<p>The link in question is:</p>
+<pre><code>http://s.vn-hd.com/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+</code></pre>
+<p>Since <code>s.vn-hd.com</code> is in our blocked domain dictionary (<code>redirects</code>), the proxy server will
+request this:</p>
+<pre><code>http://210.211.120.146/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+</code></pre>
+<p>If you try to open it directly in a browser (that isn’t using our proxy server), you’ll get a 404
+too.  Why is that? This is because the <code>Host</code> header is also changed to
+<code>210.211.120.146</code>
+instead of the original domain <code>s.vn-hd.com</code>. Normally a single web server can be serving
+multiple domains at a time, and when we send an HTTP request, we need to specify <code>Host: &lt;domain&gt;</code>
+for the server to know which domain we want to get the resource from. When the
+<code>Host</code> header is
+simply the IP, the server may get confused and therefore cannot serve the correct resource. As for
+<code>v-01.vn-hd.com</code>, we got lucky in that case.</p>
+<p>On the other hand, if we keep <code>Host: s.vn-hd.com</code> as-is, RMIT will be able to block our request.
+This leads to our final trick:</p>
+</section>
+<section id="Google-App-Engine-to-the-rescue">
+<h2>Google App Engine to the rescue!</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
+App Engine website at <code>hdviet-proxy.appspot.com</code>. It works like this:</p>
+<p><img alt="" src="hdviet_07_graph.png"></p>
+<p>Now we need to edit our server code to redirect any <code>s.vn-hd.com</code> request to
+<code>hdviet-proxy.appspot.com/?url=original_url</code>.</p>
+<pre><code class="language-python">import urllib
+
+sub_server = 's.vn-hd.com'
+remote_server = 'hdviet-proxy.appspot.com'
+redirects = {
+    'v-01.vn-hd.com': '125.212.216.93',  # video
+}
+
+def redirect(req):
+    for domain, ip in redirects.items():
+        if req.path.find(domain) != -1:
+            req.uri = req.uri.replace(domain, ip, 1)
+            req.path = req.path.replace(domain, ip, 1)
+            req.requestHeaders.setRawHeaders('host', [ip])
+            return
+        elif req.path.find(sub_server) != -1:
+            proxied_url = 'http://%s/?%s' % (remote_server,
+                                                urllib.urlencode({'url': req.uri}))
+            req.uri = proxied_url
+            req.path = req.path.replace(sub_server, remote_server)
+            req.requestHeaders.setRawHeaders('host', [remote_server])
+            return
+</code></pre>
+<p>You can view my <a href="https://github.com/nhanb/twisted-connect-proxy">finished script on github</a> and clone it to use right away.</p>
+<p>If you want to set up your own website instead of using mine, it’s really simple. Just use the new
+site template provided with GAE SDK and edit <code>main.py</code> like so:</p>
+<pre><code class="language-python">import webapp2
+from google.appengine.api import urlfetch
+
+class MainHandler(webapp2.RequestHandler):
+    def get(self):
+        url = self.request.get('url')
+        resp = urlfetch.fetch(url).content
+        self.response.write(resp)
+
+app = webapp2.WSGIApplication([
+    ('/', MainHandler)
+], debug=True)
+</code></pre>
+<p>Remember to change the <code>remote_server</code> variable in <code>server.py</code> to match your appspot link.</p>
+<p>Restart the server script, now when the browser requests for this:</p>
+<pre><code>http://s.vn-hd.com/store6/21042013/Two_and_a_Half_Men_S02/E001/Two_and_a_Half_Men_S02_E001_ENG.srt
+</code></pre>
+<p><code>server.py</code> will redirect to this:</p>
+<pre><code>http://hdviet-proxy.appspot.com/?url=http%3A%2F%2Fs.vn-hd.com%2Fstore6%2F21042013%2FTwo_and_a_Half_Men_S02%2FE001%2FTwo_and_a_Half_Men_S02_E001_ENG.srt
+</code></pre>
+<p>And the appspot site will get the original url, fetch its content, and give it right back to us:</p>
+<p><img alt="" src="hdviet_08_srt.png"></p>
+<p>You should now be able to watch movies with subtitles. Congratulations!</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/ideas/index.html b/ideas/index.html
index f5f7acc..7c9f4f4 100644
--- a/ideas/index.html
+++ b/ideas/index.html
@@ -85,7 +85,7 @@ <h2>Discord bot that launches CSGO/etc. server on demand</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/index.html b/index.html
index e9c1141..8701960 100644
--- a/index.html
+++ b/index.html
@@ -39,11 +39,56 @@ <h1 class="site-title">Hi, I&#39;m Nhân</h1>
     <a href="/chromebook/">Acer Chromebook Spin 713 &#34;Voxel&#34;: an adequate Crostini device, a buggy Linux laptop</a>
     <span class="time-suffix">(2023-04-22)</span>
   </li>
+  <li>
+    <span class="time-prefix">2023-02-12 — </span>
+    <a href="/go-stack/">Go, Postgres, Caddy, systemd: a simple, highly portable, Docker-free web stack</a>
+    <span class="time-suffix">(2023-02-12)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2022-01-30 — </span>
+    <a href="/sqlite-python/">Working with SQLite in Python without an ORM or migration framework</a>
+    <span class="time-suffix">(2022-01-30)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2021-10-02 — </span>
+    <a href="/pippable-webapp/">I made my python webapp installable via pip</a>
+    <span class="time-suffix">(2021-10-02)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2021-08-07 — </span>
+    <a href="/vim-open-link/">Opening http link under the cursor in vim</a>
+    <span class="time-suffix">(2021-08-07)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2021-02-08 — </span>
+    <a href="/stepmania-pad/">Simplest possible stepmania soft-to-hard pad mod</a>
+    <span class="time-suffix">(2021-02-08)</span>
+  </li>
   <li>
     <span class="time-prefix">2020-10-21 — </span>
     <a href="/video-streaming-3/">The video streaming finale, or why put.io is awesome</a>
     <span class="time-suffix">(2020-10-21)</span>
   </li>
+  <li>
+    <span class="time-prefix">2020-06-10 — </span>
+    <a href="/video-streaming-2/">Streaming videos from Google Drive: a second attempt</a>
+    <span class="time-suffix">(2020-06-10)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2020-05-29 — </span>
+    <a href="/mcross/">Introducing McRoss—a minimal gemini browser</a>
+    <span class="time-suffix">(2020-05-29)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2020-04-26 — </span>
+    <a href="/video-streaming-1/">Towards an acceptable video playing experience</a>
+    <span class="time-suffix">(2020-04-26)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2016-03-12 — </span>
+    <a href="/petition-fraud/">I did NOT sign that online petition!</a>
+    <span class="time-suffix">(2016-03-12)</span>
+  </li>
   <li>
     <span class="time-prefix">2016-01-23 — </span>
     <a href="/fightstick-2/">My first DIY fightstick: Part 2</a>
@@ -59,16 +104,71 @@ <h1 class="site-title">Hi, I&#39;m Nhân</h1>
     <a href="/custom-theme/">Look ma, no stock theme!</a>
     <span class="time-suffix">(2015-06-05)</span>
   </li>
+  <li>
+    <span class="time-prefix">2015-02-14 — </span>
+    <a href="/pyqt5/">How to install PyQt5 on a virtualenv on Ubuntu 14.04</a>
+    <span class="time-suffix">(2015-02-14)</span>
+  </li>
   <li>
     <span class="time-prefix">2015-01-29 — </span>
     <a href="/fcitx/">Dẹp ibus-unikey đi, dùng fcitx-unikey nhé!</a>
     <span class="time-suffix">(2015-01-29)</span>
   </li>
+  <li>
+    <span class="time-prefix">2015-01-02 — </span>
+    <a href="/pytaku-old/">Introducing Pytaku—the only online manga reader you&#39;ll ever need</a>
+    <span class="time-suffix">(2015-01-02)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2014-12-16 — </span>
+    <a href="/virtualenvwrapper/">Virtualenv(wrapper), python2 and python3</a>
+    <span class="time-suffix">(2014-12-16)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2014-08-02 — </span>
+    <a href="/tmux-italics/">Enable italic text inside vim inside tmux inside gnome-terminal</a>
+    <span class="time-suffix">(2014-08-02)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2014-05-01 — </span>
+    <a href="/node-webkit/">Setting up your development environment for a node-webkit project</a>
+    <span class="time-suffix">(2014-05-01)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2014-03-17 — </span>
+    <a href="/hdviet/">How I bypassed my university&#39;s domain blocker to watch movies on hdviet.com</a>
+    <span class="time-suffix">(2014-03-17)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2014-02-04 — </span>
+    <a href="/manjaro-xfce/">What I did after installing Manjaro xfce</a>
+    <span class="time-suffix">(2014-02-04)</span>
+  </li>
   <li>
     <span class="time-prefix">2014-01-25 — </span>
     <a href="/cool/">&#34;Have you built anything cool?&#34;</a>
     <span class="time-suffix">(2014-01-25)</span>
   </li>
+  <li>
+    <span class="time-prefix">2013-09-06 — </span>
+    <a href="/ubuntu-programs/">Installing programs in Ubuntu</a>
+    <span class="time-suffix">(2013-09-06)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2013-06-17 — </span>
+    <a href="/rmit-wifi/">Fix RMIT wi-fi issue in Ubuntu 13.04 and variants</a>
+    <span class="time-suffix">(2013-06-17)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2013-06-07 — </span>
+    <a href="/linux-automation/">Why I use Linux: Automation</a>
+    <span class="time-suffix">(2013-06-07)</span>
+  </li>
+  <li>
+    <span class="time-prefix">2013-05-13 — </span>
+    <a href="/pathogen-vs-vundle/">Modern vim plugin management: Pathogen vs Vundle</a>
+    <span class="time-suffix">(2013-05-13)</span>
+  </li>
 </ul>
 
 
@@ -121,7 +221,7 @@ <h1 class="site-title">Hi, I&#39;m Nhân</h1>
 </style>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/linux-automation/index.dj b/linux-automation/index.dj
index 3158c1f..32728b6 100644
--- a/linux-automation/index.dj
+++ b/linux-automation/index.dj
@@ -1,18 +1,15 @@
 Title: Why I use Linux: Automation
-Date: 2013-06-07 08:02
-Category: tutorials
-Tags: python, linux
-Slug: why-i-use-linux-automation
-Summary: Repeating is for losers.
+PostedAt: 2013-06-07 08:02
+---
 
 (In this post, when I say Linux, I mean any popular GNU/Linux distribution. Hope this clarification
 will keep the nitpickers away.)
 
-First let's discuss *why* automation rocks.
+First let's discuss _why_ automation rocks.
 
 ## Repetition is evil (and boring)
 
-As a (would-be) software engineer, the *repetition is evil* notion has been planted in my head for
+As a (would-be) software engineer, the _repetition is evil_ notion has been planted in my head for
 far more times than anything else, and for good reasons.
 
 People are far more prone to error than computers, and doing repetitive tasks creates just too
@@ -22,7 +19,7 @@ with extreme speed and accuracy.
 Moreover, let's face it: We developers are all (or at least mostly) lazy. Not the "I'm don't
 wanna do anything" kind of lazy, but more of the "This crap is boring and not challenging at all,
 why the hell am I wasting time for it?" type. We've all got better things to do with our lives,
-like re-watching the last episode of BBC's *Sherlock* to look for clues to how he faked his death,
+like re-watching the last episode of BBC's _Sherlock_ to look for clues to how he faked his death,
 or trying to figure out what that "Han shot first" meme means (sorry, I'm from the later
 generation).
 
@@ -49,25 +46,26 @@ beginner course from [justinguitar](http://www.justinguitar.com).
 
 The real flow starts from line 48:
 
-    :::python
-    # Fetch index pages which has links to all beginner lessons
-    r = requests.get('http://www.justinguitar.com/en/BC-000-BeginnersCourse.php')
-    start_page = r.text
+```python
+# Fetch index pages which has links to all beginner lessons
+r = requests.get('http://www.justinguitar.com/en/BC-000-BeginnersCourse.php')
+start_page = r.text
 
-    # Search for all links to lessons
-    pat = re.compile('<a href="(BC-[0-9]{3}-.+?)"')
-    pages = pat.findall(start_page)
+# Search for all links to lessons
+pat = re.compile('<a href="(BC-[0-9]{3}-.+?)"')
+pages = pat.findall(start_page)
 
-    # Fetch html for each lesson
-    pages_html = fetch_html(pages)
+# Fetch html for each lesson
+pages_html = fetch_html(pages)
 
-    # Crawl each lesson page, pull out lesson names and youtube link code
-    youtube_codes = []
-    for html in pages_html:
-        code = parse_info(html)
-        print code
-        if code not in youtube_codes:
-            youtube_codes.append(code)
+# Crawl each lesson page, pull out lesson names and youtube link code
+youtube_codes = []
+for html in pages_html:
+    code = parse_info(html)
+    print code
+    if code not in youtube_codes:
+        youtube_codes.append(code)
+```
 
 To summarize, this snippet goes to justinguitar's beginner course index page, grab all links to
 each lesson, then grab the lesson title as well as the youtube video code to its video. The
@@ -79,30 +77,31 @@ tool itself can download the video too, but it doesn't support multiple connecti
 accelerate the download. This is where `aria2c` jumps in: it takes the direct link from
 `youtube-dl` then download the whole thing:
 
-    :::python
-    # Leech the hell out of them
-    for lesson in youtube_codes:
-
-        # Ignore if lesson has no video
-        if lesson[1] == None:
-            call(['touch', lesson[0]])
-            continue
-
-        # Use youtube-dl to get fresh download link and file extension
-        command = 'youtube-dl ' + lesson[1] +\
-                ' --skip-download --get-url --get-filename -f 35/34/82/44/43/100'
-
-        shell_output = str(check_output(command.split()))
-        direct_link, fname = shell_output.splitlines()
-        file_ext = fname[fname.rfind('.'):]
-        file_name = lesson[0] + file_ext
-
-        # Then aria2 for serious multi-part download acceleration
-        print 'Downloading ' + file_name + '...'
-        command = ['aria2c', "-o", file_name, '-x2',
-                "%s" % direct_link]
-        shell_output = check_output(command)
-        print shell_output
+```python
+# Leech the hell out of them
+for lesson in youtube_codes:
+
+    # Ignore if lesson has no video
+    if lesson[1] == None:
+        call(['touch', lesson[0]])
+        continue
+
+    # Use youtube-dl to get fresh download link and file extension
+    command = 'youtube-dl ' + lesson[1] +\
+            ' --skip-download --get-url --get-filename -f 35/34/82/44/43/100'
+
+    shell_output = str(check_output(command.split()))
+    direct_link, fname = shell_output.splitlines()
+    file_ext = fname[fname.rfind('.'):]
+    file_name = lesson[0] + file_ext
+
+    # Then aria2 for serious multi-part download acceleration
+    print 'Downloading ' + file_name + '...'
+    command = ['aria2c', "-o", file_name, '-x2',
+            "%s" % direct_link]
+    shell_output = check_output(command)
+    print shell_output
+```
 
 That's it! I just needed to launch this script, turn off the laptop screen and go to bed. This
 morning I woke up seeing the whole course with almost 100 lessons downloaded. Imagine having to
diff --git a/linux-automation/index.html b/linux-automation/index.html
new file mode 100644
index 0000000..95a49d4
--- /dev/null
+++ b/linux-automation/index.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Why I use Linux: Automation | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2013-06-07">
+        Friday, 07 Jun 2013
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Why I use Linux: Automation</h1>
+<p>(In this post, when I say Linux, I mean any popular GNU/Linux distribution. Hope this clarification
+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>
+<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
+much room for that. Computers, on the other hand, do everything exactly how you tell them to do,
+with extreme speed and accuracy.</p>
+<p>Moreover, let’s face it: We developers are all (or at least mostly) lazy. Not the “I’m don’t
+wanna do anything” kind of lazy, but more of the “This crap is boring and not challenging at all,
+why the hell am I wasting time for it?” type. We’ve all got better things to do with our lives,
+like re-watching the last episode of BBC’s <em>Sherlock</em> to look for clues to how he faked his death,
+or trying to figure out what that “Han shot first” meme means (sorry, I’m from the later
+generation).</p>
+</section>
+<section id="Automation-needs-command-line-tools">
+<h2>Automation needs command line tools</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>
+<p>And this is where Windows falls short. Most (if not all) Windows tools are designed for GUI, and
+the whole Windows ecosystem is built around GUI use.</p>
+<p>It’s a whole different matter in Linux: from the good old awk, sed, grep, wget to the new shiny
+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>
+<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
+<a href="https://gist.github.com/nhanb/5726342">python script</a> I wrote last night to download the whole
+beginner course from <a href="http://www.justinguitar.com">justinguitar</a>.</p>
+<p>The real flow starts from line 48:</p>
+<pre><code class="language-python"># Fetch index pages which has links to all beginner lessons
+r = requests.get('http://www.justinguitar.com/en/BC-000-BeginnersCourse.php')
+start_page = r.text
+
+# Search for all links to lessons
+pat = re.compile('&lt;a href="(BC-[0-9]{3}-.+?)"')
+pages = pat.findall(start_page)
+
+# Fetch html for each lesson
+pages_html = fetch_html(pages)
+
+# Crawl each lesson page, pull out lesson names and youtube link code
+youtube_codes = []
+for html in pages_html:
+    code = parse_info(html)
+    print code
+    if code not in youtube_codes:
+        youtube_codes.append(code)
+</code></pre>
+<p>To summarize, this snippet goes to justinguitar’s beginner course index page, grab all links to
+each lesson, then grab the lesson title as well as the youtube video code to its video. The
+result is the list name <code>youtube_codes</code>; each element is a tuple with the format
+<code>(title, youtube_code)</code>.</p>
+<p>Then I use a command line tool called <code>youtube-dl</code> to fetch the direct link to each video. The
+tool itself can download the video too, but it doesn’t support multiple connections to
+accelerate the download. This is where <code>aria2c</code> jumps in: it takes the direct link from
+<code>youtube-dl</code> then download the whole thing:</p>
+<pre><code class="language-python"># Leech the hell out of them
+for lesson in youtube_codes:
+
+    # Ignore if lesson has no video
+    if lesson[1] == None:
+        call(['touch', lesson[0]])
+        continue
+
+    # Use youtube-dl to get fresh download link and file extension
+    command = 'youtube-dl ' + lesson[1] +\
+            ' --skip-download --get-url --get-filename -f 35/34/82/44/43/100'
+
+    shell_output = str(check_output(command.split()))
+    direct_link, fname = shell_output.splitlines()
+    file_ext = fname[fname.rfind('.'):]
+    file_name = lesson[0] + file_ext
+
+    # Then aria2 for serious multi-part download acceleration
+    print 'Downloading ' + file_name + '...'
+    command = ['aria2c', "-o", file_name, '-x2',
+            "%s" % direct_link]
+    shell_output = check_output(command)
+    print shell_output
+</code></pre>
+<p>That’s it! I just needed to launch this script, turn off the laptop screen and go to bed. This
+morning I woke up seeing the whole course with almost 100 lessons downloaded. Imagine having to
+manually download all that by clicking each link… You don’t wanna go there, do you?</p>
+<p>So that’s just a very simple example of what automation helps your every day life. Of course its
+true power is unleashed when used in development; this is how one-click test and deployment
+works. Windows can do this too, but your choice of tool will be limited. And don’t get me started
+on its lack of a decent package manager!</p>
+<p>To make a long story short, do yourself a favor and install a Linux distro.</p>
+<p>… or buy a Mac.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/manifest.txt b/manifest.txt
index efed4a3..42362c0 100644
--- a/manifest.txt
+++ b/manifest.txt
@@ -6,9 +6,18 @@ fcitx/index.html
 feed.xml
 fightstick-1/index.html
 fightstick-2/index.html
+go-stack/index.html
+hdviet/index.html
 ideas/index.html
 index.html
+linux-automation/index.html
+manjaro-xfce/index.html
+mcross/index.html
+node-webkit/index.html
 notes/index.html
+pathogen-vs-vundle/index.html
+petition-fraud/index.html
+pippable-webapp/index.html
 posts/acer-chromebook-spin/index.html
 posts/enable-italic-text-vim-tmux-gnome-terminal/index.html
 posts/fix-rmit-wifi-issue-in-ubuntu-13-04-and-variants/index.html
@@ -37,5 +46,16 @@ posts/what-i-did-after-installing-manjaro-xfce/index.html
 posts/why-i-use-linux-automation/index.html
 posts/working-with-sqlite-in-python-without-an-orm-or-migration-framework/index.html
 projects/index.html
+pyqt5/index.html
+pytaku-old/index.html
+rmit-wifi/index.html
+sqlite-python/index.html
+stepmania-pad/index.html
+tmux-italics/index.html
+ubuntu-programs/index.html
+video-streaming-1/index.html
+video-streaming-2/index.html
 video-streaming-3/index.html
+vim-open-link/index.html
+virtualenvwrapper/index.html
 yaks/index.html
\ No newline at end of file
diff --git a/manjaro-xfce/index.dj b/manjaro-xfce/index.dj
index 55350a6..43bd4da 100644
--- a/manjaro-xfce/index.dj
+++ b/manjaro-xfce/index.dj
@@ -1,11 +1,9 @@
 Title: What I did after installing Manjaro xfce
-Slug: what-i-did-after-installing-manjaro-xfce
-Date: 2014-02-04 20:20:13
-Category: tutorials
-Tags: arch, linux
+PostedAt: 2014-02-04 20:20:13
+---
 
 After about 2 months with elementary OS, I got sick of the guaranteed once-every-hour crashes of
-its **Files** file manager (yeah, I'm still hating their naming decisions with a passion), the
+its *Files* file manager (yeah, I'm still hating their naming decisions with a passion), the
 flickering when I play fullscreen OpenGL games, and the automatic collapsing of workspaces. I've
 had enough of that. Let's go back to xfce! But hey, (X)ubuntu 14.04 is nearly out but I don't want
 to install an alpha version right now, and installing 13.10 just to update 2 months later is insane
@@ -21,15 +19,16 @@ It happened to me when I tried to mount my existing `/home` partition. Instead o
 screenshot featured on Manjaro's home page, I got something like this (image courtesy of Xfce
 project website):
 
-![](/images/xfce_default.jpg)
+![](xfce_default.jpg)
 
 I guess it was because of some weird bug that the partition ended up being owned by `root` so the
 installer could not copy Manjaro-specific settings at the end. Make it your own again then copy the
 default Manjaro files:
 
-    :::bash
-    sudo chown $USER /home
-    cp -a /etc/skel/. ~/
+```sh
+sudo chown $USER /home
+cp -a /etc/skel/. ~/
+```
 
 Then restart your computer and see if it worked (it should).
 
@@ -37,15 +36,16 @@ Then restart your computer and see if it worked (it should).
 
 Getting Micro$oft fonts is like the first thing to do after any Linux distro installation. The Arch
 community has a whole [wiki page][1] dedicated to it. It's worth mentioning that you can't
-*legally* install those packages without the actual fonts already on your computer. Assuming you
+_legally_ install those packages without the actual fonts already on your computer. Assuming you
 have an installed copy of Windows 7, go to its `Fonts` folder and put the necessary fonts in the
 same folder of the extracted package downloaded from the AUR page. For some instant copy-and-paste
-shell commands: (**warning**: this script assumes you already have all your Windows 7 fonts in
+shell commands: (*warning*: this script assumes you already have all your Windows 7 fonts in
 `~/win_fonts/`. Put them there before running the following commands)
 
-    :::bash
-    curl -O 'https://gist.github.com/nhanb/8804875/raw/arch-ms-fonts.sh'
-    bash arch-ms-fonts.sh
+```sh
+curl -O 'https://gist.github.com/nhanb/8804875/raw/arch-ms-fonts.sh'
+bash arch-ms-fonts.sh
+```
 
 ## Proper font smoothing
 
@@ -58,14 +58,15 @@ Even if you're not Japanese or Korean, you'll occasionally come across content t
 characters from these languages. With the default installation, all those characters will be shown
 as rectangles, which bugs me a lot.
 
-![](/images/jap_font_none.png)
+![](jap_font_none.png)
 
 The solution? Simple. Just install the `ttf-droid` package:
 
-    :::bash
-    sudo pacman -S ttf-droid
+```sh
+sudo pacman -S ttf-droid
+```
 
-![](/images/jap_font_done.png)
+![](jap_font_done.png)
 
 Now that's better!
 
diff --git a/manjaro-xfce/index.html b/manjaro-xfce/index.html
new file mode 100644
index 0000000..a29c7fa
--- /dev/null
+++ b/manjaro-xfce/index.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>What I did after installing Manjaro xfce | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2014-02-04">
+        Tuesday, 04 Feb 2014
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>What I did after installing Manjaro xfce</h1>
+<p>After about 2 months with elementary OS, I got sick of the guaranteed once-every-hour crashes of
+its <strong>Files</strong> file manager (yeah, I’m still hating their naming decisions with a passion), the
+flickering when I play fullscreen OpenGL games, and the automatic collapsing of workspaces. I’ve
+had enough of that. Let’s go back to xfce! But hey, (X)ubuntu 14.04 is nearly out but I don’t want
+to install an alpha version right now, and installing 13.10 just to update 2 months later is insane
+(to me, at least). That’s when I noticed <a href="http://manjaro.org/">Manjaro</a> - a battery-included distro based on Arch.
+All hail rolling release!</p>
+<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>
+<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>
+<p><img alt="" src="xfce_default.jpg"></p>
+<p>I guess it was because of some weird bug that the partition ended up being owned by <code>root</code> so the
+installer could not copy Manjaro-specific settings at the end. Make it your own again then copy the
+default Manjaro files:</p>
+<pre><code class="language-sh">sudo chown $USER /home
+cp -a /etc/skel/. ~/
+</code></pre>
+<p>Then restart your computer and see if it worked (it should).</p>
+</section>
+<section id="Get-Mirosoft-fonts">
+<h2>Get Mirosoft fonts</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
+have an installed copy of Windows 7, go to its <code>Fonts</code> folder and put the necessary fonts in the
+same folder of the extracted package downloaded from the AUR page. For some instant copy-and-paste
+shell commands: (<strong>warning</strong>: this script assumes you already have all your Windows 7 fonts in
+<code>~/win_fonts/</code>. Put them there before running the following commands)</p>
+<pre><code class="language-sh">curl -O 'https://gist.github.com/nhanb/8804875/raw/arch-ms-fonts.sh'
+bash arch-ms-fonts.sh
+</code></pre>
+</section>
+<section id="Proper-font-smoothing">
+<h2>Proper font smoothing</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>
+<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>
+<p><img alt="" src="jap_font_none.png"></p>
+<p>The solution? Simple. Just install the <code>ttf-droid</code> package:</p>
+<pre><code class="language-sh">sudo pacman -S ttf-droid
+</code></pre>
+<p><img alt="" src="jap_font_done.png"></p>
+<p>Now that’s better!</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/jap_font_done.png b/manjaro-xfce/jap_font_done.png
similarity index 100%
rename from content/images/jap_font_done.png
rename to manjaro-xfce/jap_font_done.png
diff --git a/content/images/jap_font_none.png b/manjaro-xfce/jap_font_none.png
similarity index 100%
rename from content/images/jap_font_none.png
rename to manjaro-xfce/jap_font_none.png
diff --git a/content/images/xfce_default.jpg b/manjaro-xfce/xfce_default.jpg
similarity index 100%
rename from content/images/xfce_default.jpg
rename to manjaro-xfce/xfce_default.jpg
diff --git a/mcross/index.dj b/mcross/index.dj
index ff94066..48cdfba 100644
--- a/mcross/index.dj
+++ b/mcross/index.dj
@@ -1,7 +1,6 @@
 Title: Introducing McRoss—a minimal gemini browser
-Date: 2020-05-29 09:44
-Category: side projects
-
+PostedAt: 2020-05-29 09:44
+---
 
 The last couple of months saw the first "PR" wave of [the gemini protocol][1]
 on the usual online [tech][2][(bro)][3] forums. Its pitch is simple: the web
@@ -13,15 +12,14 @@ Sure I agree the web is [comically bloated][4], [openly user-hostile][5], and
 the big players are only [adding to the problem][7], but the fact remains that
 the web is the most convenient thing there is, both from a user's and
 developer's perspective. Gemini is a fun experiment. It may even be a hit among
-<strike>nerds</strike> power users and the overly privacy-concious, but that's
-it.
+{-nerds-} power users and the overly privacy-concious, but that's it.
 
-But then again, I consider myself among the "<strike>nerds</strike> power users
+But then again, I consider myself among the "{-nerds-} power users
 and the overly privacy-concious" demographic, so I naturally want to see what
 cool stuff people on the gemini-verse are up to. Therefore, I need a gemini
 browser. _Naturally_, I [wrote one][12]:
 
-![McRoss Browser screenshot](/images/mcross_01_screenshot.png "")
+![McRoss Browser screenshot](mcross_01_screenshot.png)
 
 At this stage it can browse plaintext and gemini content, but not binary yet.
 It also doesn't verify TLS certificates, because turns out [in the gemini
@@ -57,7 +55,7 @@ should the program hang or crash without displaying a proper message.
 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:
 
-![Castor links](/images/mcross_02_castor.png)
+![Castor links](mcross_02_castor.png)
 
 Another admittedly petty issue I have is that it's GTK while I'm using KDE
 Plasma, and although KDE has a compatibility layer that tries to render GTK
diff --git a/mcross/index.html b/mcross/index.html
new file mode 100644
index 0000000..e3ffc3b
--- /dev/null
+++ b/mcross/index.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Introducing McRoss—a minimal gemini browser | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2020-05-29">
+        Friday, 29 May 2020
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Introducing McRoss—a minimal gemini browser</h1>
+<p>The last couple of months saw the first “PR” wave of <a href="https://gemini.circumlunar.space/">the gemini protocol</a>
+on the usual online <a href="https://lobste.rs/s/79pu7o/gemini_protocol_inbetween_gopher_web">tech</a><a href="https://news.ycombinator.com/item?id=23042424">(bro)</a> forums. Its pitch is simple: the web
+has gone out of hand, gopher is too barebones and insecure by default, here’s a
+new thing that sits in the middle.</p>
+<p>Personally I’m skeptical if this thing will take off any time soon (or ever).
+Sure I agree the web is <a href="https://idlewords.com/talks/website_obesity.htm">comically bloated</a>, <a href="https://neustadt.fr/essays/against-a-user-hostile-web/">openly user-hostile</a>, and
+the big players are only <a href="https://developers.google.com/amp">adding to the problem</a>, but the fact remains that
+the web is the most convenient thing there is, both from a user’s and
+developer’s perspective. Gemini is a fun experiment. It may even be a hit among
+<del>nerds</del> power users and the overly privacy-concious, but that’s it.</p>
+<p>But then again, I consider myself among the “<del>nerds</del> power users
+and the overly privacy-concious” demographic, so I naturally want to see what
+cool stuff people on the gemini-verse are up to. Therefore, I need a gemini
+browser. <em>Naturally</em>, I <a href="https://sr.ht/~nhanb/mcross/">wrote one</a>:</p>
+<p><img alt="McRoss Browser screenshot" src="mcross_01_screenshot.png"></p>
+<p>At this stage it can browse plaintext and gemini content, but not binary yet.
+It also doesn’t verify TLS certificates, because turns out <a href="https://todo.sr.ht/~nhanb/mcross/1">in the gemini
+world</a> it’s preferable for browser to use self-signed certs and expect
+clients to trust on first use (TOFU), just like how basic SSH works. I haven’t
+implemented TOFU yet so the browser trusts whatever and is vulnerable to MITM
+attacks for every request. It’s highly unlikely that anyone would bother to,
+but take everything you read with a pinch of salt anyway.</p>
+<p>Why not use one of the existing browsers you ask? Sure enough there are a bunch
+of existing browsers, with <a href="https://sr.ht/~julienxx/Castor/">Castor</a> appearing to be the furthest along in
+development, but it didn’t work <em>quite</em> the way I would like. This made me want
+to find out for myself just how hard it is to build a reasonably user-friendly
+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>
+<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
+ridiculously elementary considering that’s how, say, all Windows 95 programs
+worked, but here we are two decades and a half later and the Castor browser
+just completely freezes the GUI during every network request.</p>
+<p>With McRoss I intentionally put the GUI and I/O event loops in their separate
+threads to make sure the program’s always responsive. I also paid attention to
+small details like the loading cursor and real-time status bar. At no point
+should the program hang or crash without displaying a proper message.</p>
+</section>
+<section id="Aesthetics">
+<h3>Aesthetics:</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>
+<p>Another admittedly petty issue I have is that it’s GTK while I’m using KDE
+Plasma, and although KDE has a compatibility layer that tries to render GTK
+widgets as close to KDE counterparts as possible, the result is still…
+subpar.</p>
+<p>McRoss on the other hand uses the tk gui toolkit, and as of tk 8.6, it
+automatically gives you the native look and feel on Windows and Mac OS (well,
+not automatically but it takes trivial work anyway). Linux however doesn’t have
+such a thing, but the bundled <code>clam</code> theme looks pleasing enough for me. Yes, I
+do think a retro looking theme fares better than the gtk-on-kde look, and its
+simple scrollbar looks and, more importantly, <em>works</em> way better than those
+nigh-unclickable abominations that KDE and GTK call their “modern scrollbar”,
+fight me.</p>
+<p>Another explicit design decision in McRoss is that while custom styling is
+applied to special lines (heading, list, code block…), their textual content
+is kept the same as source, with the special characters (<code>#</code>, <code>*</code>, etc.)
+intact. This way when someone has read a gemini page, they already know how to
+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>
+<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
+executables so users are supposed to install the Rust toolchain then build
+Castor themselves. Compiling a gtk-enabled Rust project is… not a quick
+affair.</p>
+<p>McRoss is currently packaged as a well-behaved PyPI package and can be
+installed with <code>pip3 install mcross</code>. Its only dependencies are the standard
+library and <code>curio</code> so installation should be super fast. I know I know,
+requiring python in the first place is its own can of worms. I do plan to
+improve the situation with “frozen” executables some time down the line.</p>
+</section>
+<section id="Closing-thoughts">
+<h1>Closing thoughts</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
+MMORPGs, of crappy Yahoo! 360 blogs riced up with copy-pasted html/css all over
+the place, of numerous Vietnamese warez forums powered by pirated vBulletin
+running on shady free shared CPanel hosts, of monthly Drupal/Joomla SQL
+injection zero-days. It was truly the wild wild web, insanely accessible,
+insanely unsafe, and insanely fun. It was the web where a young clueless
+teenage me could find fun random stuff everyday, put fun random stuff out
+there for everyone to see, no matter how shitty and unsecure they are, because
+it didn’t matter if I get pwn’d: my life back then wasn’t that much dependent
+on the web.</p>
+<p>Can I get all that back? I think not. The web, or more broadly, the internet
+grew up (to be a nasty adult, but an adult nevertheless), just like anything
+where there’s enough profit to be made. I’m not saying it’s a bad thing (hell,
+I make a living out of building webstuff), but it is undeniably a sad thing.
+Gemini may be a spark that begins a push back against unjustified complexity,
+or it may end up being just another niche tech curiosity. I’m leaning towards
+the latter, but in the meantime, I’ll keep peeking at the geminiverse with my
+comfy trusty browser.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/mcross_01_screenshot.png b/mcross/mcross_01_screenshot.png
similarity index 100%
rename from content/images/mcross_01_screenshot.png
rename to mcross/mcross_01_screenshot.png
diff --git a/content/images/mcross_02_castor.png b/mcross/mcross_02_castor.png
similarity index 100%
rename from content/images/mcross_02_castor.png
rename to mcross/mcross_02_castor.png
diff --git a/node-webkit/index.dj b/node-webkit/index.dj
index 453b671..379fb58 100644
--- a/node-webkit/index.dj
+++ b/node-webkit/index.dj
@@ -1,10 +1,8 @@
 Title: Setting up your development environment for a node-webkit project
-Date: 2014-05-01 08:23
-Category: tutorials
-Tags: linux, webdev
-Slug: setting-up-your-development-environment-for-a-node-webkit-project
+PostedAt: 2014-05-01 08:23
+---
 
-![](/images/nw_xp.png "XP support y'all!")
+![](nw_xp.png)
 
 [Node-webkit][1] lets you write cross-platform (Mac + Linux + Winbloze) desktop applications using
 HTML5 and nodejs. That's a fancy way of saying "a webkit wrapper that also gives you filesystem
@@ -30,8 +28,9 @@ This tutorial is like a stripped down version of that. In the end we'll have:
 Follow the README on [node-webkit's GitHub page][1] to download a precompiled `nw` binary for your
 platform. If you're using Arch Linux, you're in luck since there's already an AUR package:
 
-    :::bash
-    $ yaourt -S node-webkit
+```sh
+$ yaourt -S node-webkit
+```
 
 If you're on Ubuntu or some other repo and you get some error about `libudev.so.0`, [read this][3]
 for a hotfix.
@@ -41,52 +40,56 @@ The rest of this tutorial will assume that you have `nw` accessible as an execut
 ## Running an app
 
 First, take a look at nw's [quickstart guide][4]. We'll make a somewhat different structure,
-allowing the **dist** directory to store our binary releases:
-
-    :::
-    ├── app/
-    │  ├── css/
-    │  ├── js/
-    │  ├── index.html
-    │  └── package.json
-    └── dist/
-
-**package.json** stores the information that `nw` requires. Its content goes like this:
-
-    :::json
-    {
-      "name": "your-project-name",
-      "version": "0.0.1",
-      "main": "index.html",
-      "window": {
-          "toolbar": true
-      }
+allowing the *dist* directory to store our binary releases:
+
+```
+├── app/
+│  ├── css/
+│  ├── js/
+│  ├── index.html
+│  └── package.json
+└── dist/
+```
+
+*package.json* stores the information that `nw` requires. Its content goes like this:
+
+```json
+{
+    "name": "your-project-name",
+    "version": "0.0.1",
+    "main": "index.html",
+    "window": {
+        "toolbar": true
     }
+}
+```
 
 `window.toolbar` is `true` by default so we don't actually need it. Switch it to `false` if you
 want to hide the browser-like address bar.
 
 To embed css/js files, don't use absolute urls. Use relative ones like this:
 
-    :::html
-    <link href="css/style.css" rel="stylesheet">
+```html
+<link href="css/style.css" rel="stylesheet">
+```
 
 There's no point in using CDNs because we're distributing the whole app with its assets as a
 one-time download. Things like automatic css/js minification or concatination aren't needed either.
 For simplicity's sake, we'll just download a minified version of whatever js/css library that we
-need and stuff them into **css** or **js** dir. For example: (**dist** directory omitted)
-
-    :::
-    app/
-    ├── css
-    │   └── bootstrap.min.css
-    ├── js
-    │   ├── bootstrap.min.js
-    │   ├── knockout.min.js
-    │   ├── jquery.min.js
-    │   └── app.js
-    ├── index.html
-    └── package.json
+need and stuff them into *css* or *js* dir. For example: (*dist* directory omitted)
+
+```
+app/
+├── css
+│   └── bootstrap.min.css
+├── js
+│   ├── bootstrap.min.js
+│   ├── knockout.min.js
+│   ├── jquery.min.js
+│   └── app.js
+├── index.html
+└── package.json
+```
 
 If you're experienced in front-end web development tools, feel free to go wild with bower,
 grunt/gulp/whatever. Again, check out the [node-webkit-hipster-seed][2] project if you know what
@@ -95,13 +98,15 @@ you're doing.
 You can now test run your app with the `nw <directory>` command. In our case: `nw app`. Notice the
 weird url, which is why we can't use absolute urls in the first place:
 
-    :::
-    file:///home/nhanb/Dropbox/small_projects/ajmg-nw/app/index.html
+```
+file:///home/nhanb/Dropbox/small_projects/ajmg-nw/app/index.html
+```
 
 Once you've packaged your app to a single executable, the url will be something like this:
 
-    :::
-    file:///tmp/.org.chromium.Chromium.IJWqkq/index.html
+```
+file:///tmp/.org.chromium.Chromium.IJWqkq/index.html
+```
 
 But let's not get ahead of ourselves. Let's solve the most obvious dev issue first:
 
@@ -116,7 +121,7 @@ Chrome certainly doesn't offer. Therefore, the only way to check out how the app
 using `nw app`.
 
 We'll use `livereload` to make automatic reloading possible. The idea is quite simple: we fire off
-a `livereload` daemon that watches for any change in our **app/** directory. In our app, we embed a
+a `livereload` daemon that watches for any change in our *app/* directory. In our app, we embed a
 certain piece of javascript that connects to that `livereload` daemon and refreshes the page
 whenever a "change" event is broadcast.
 
@@ -124,50 +129,55 @@ There are many `livereload` daemon implementations. Considering the fact that mo
 have python and pip installed, let's go with the `livereload` pip package (it's only compatible
 with python2, by the way). If you're on Ubuntu and don't know what I'm talking about:
 
-    :::bash
-    $ sudo apt-get install python-pip
-    $ sudo pip install livereload
+```sh
+$ sudo apt-get install python-pip
+$ sudo pip install livereload
+```
 
 There are ruby/javascript implementations too. Google them if you prefer those things.
 
 Either way, we can now fire off a livereload server:
 
-    :::
-    # I don't know why but seems like the python implementation doesn't work
-    # when I type `livereload app`. Weird.
-    $ cd app
-    $ livereload .
+```sh
+# I don't know why but seems like the python implementation doesn't work
+# when I type `livereload app`. Weird.
+$ cd app
+$ livereload .
+```
 
 Now how do we inject the livereload javascript? On Google Chrome there is an official livereload
 plugin, but we're using node-webkit so that's not possible. No problem! The `livereload` daemon we
 fired off earlier is actually a web server which also serves the necessary livereload client
-JavaScript snippet too. Simply embed it to your **index.html**:
+JavaScript snippet too. Simply embed it to your *index.html*:
 
-    :::html
-    <script src="http://localhost:35729/livereload.js"></script>
+```html
+<script src="http://localhost:35729/livereload.js"></script>
+```
 
 Fire off the app with `nw app` again and you'll have automatic reloading. Cool eh?
 
 Another problem: we only want livereload in our development version, not in the released app. Let's
-modify our javascript snippet in **index.html** to only load livereload when a certain environment
+modify our javascript snippet in *index.html* to only load livereload when a certain environment
 variable is set to `1`:
 
-    :::html
-    <script>
-    // Load livereload if in dev environment
-    if (process.env.NW_DEV_MY_AWESOME_PROJECT == 1) {
-        var script = document.createElement('script');
-        script.type = 'text/javascript';
-        script.src = 'http://localhost:35729/livereload.js';
-        document.body.appendChild(script);
-    }
-    </script>
+```html
+<script>
+// Load livereload if in dev environment
+if (process.env.NW_DEV_MY_AWESOME_PROJECT == 1) {
+    var script = document.createElement('script');
+    script.type = 'text/javascript';
+    script.src = 'http://localhost:35729/livereload.js';
+    document.body.appendChild(script);
+}
+</script>
+```
 
 Now to start the app:
 
-    :::bash
-    $ export NW_DEV_MY_AWESOME_PROJECT=1
-    $ nw app
+```sh
+$ export NW_DEV_MY_AWESOME_PROJECT=1
+$ nw app
+```
 
 ## Simple cross-platform build command
 
@@ -177,71 +187,75 @@ article][5] if you prefer the do-it-yourself way.
 But if you're lazy (like me) and don't have a problem using nodejs/grunt, just use the excellent
 [grunt-node-webkit-builder][6]. Again, for newcomers using Ubuntu:
 
-    :::bash
-    $ sudo add-apt-repository ppa:chris-lea/node.js
-    $ sudo apt-get update
-    $ sudo apt-get install python-software-properties python g++ make nodejs
-    $ sudo npm install -g grunt-cli
-    $ # cd to your project root (the one containing app/ and dist/)
+```sh
+$ sudo add-apt-repository ppa:chris-lea/node.js
+$ sudo apt-get update
+$ sudo apt-get install python-software-properties python g++ make nodejs
+$ sudo npm install -g grunt-cli
+$ # cd to your project root (the one containing app/ and dist/)
+```
 
 Create `package.json` and `Gruntfile.js` in project root:
 
-    ├── app/
-    │   ├── css/
-    │   ├── js/
-    │   ├── index.html
-    │   └── package.json
-    ├── dist/
-    ├── Gruntfile.js
-    └── package.json
-
-**package.json**:
-
-    :::json
-    {
-      "name": "whatever",
-      "version": "0.0.1",
-      "description": "Whatever",
-      "author": "Bui Thanh Nhan"
-    }
-
-**Gruntfile.js**:
-
-    :::js
-    module.exports = function(grunt) {
-        grunt.initConfig({
-            pkg: grunt.file.readJSON('package.json'),
-            nodewebkit: {
-                options: {
-                    build_dir: './dist',
-                    // choose what platforms to compile for here
-                    mac: true,
-                    win: true,
-                    linux32: true,
-                    linux64: true
-                },
-                src: ['./app/**/*']
-            }
-        })
-
-        grunt.loadNpmTasks('grunt-node-webkit-builder');
-        grunt.registerTask('default', ['nodewebkit']);
-    };
+```
+├── app/
+│   ├── css/
+│   ├── js/
+│   ├── index.html
+│   └── package.json
+├── dist/
+├── Gruntfile.js
+└── package.json
+```
+
+*package.json*:
+
+```
+{
+    "name": "whatever",
+    "version": "0.0.1",
+    "description": "Whatever",
+    "author": "Bui Thanh Nhan"
+}
+```
+
+*Gruntfile.js*:
+
+```javascript
+module.exports = function(grunt) {
+    grunt.initConfig({
+        pkg: grunt.file.readJSON('package.json'),
+        nodewebkit: {
+            options: {
+                build_dir: './dist',
+                // choose what platforms to compile for here
+                mac: true,
+                win: true,
+                linux32: true,
+                linux64: true
+            },
+            src: ['./app/**/*']
+        }
+    })
+
+    grunt.loadNpmTasks('grunt-node-webkit-builder');
+    grunt.registerTask('default', ['nodewebkit']);
+};
+```
 
 Then:
 
-    :::bash
-    $ npm install grunt grunt-node-webkit-builder --save-dev
-    $ grunt
+```sh
+$ npm install grunt grunt-node-webkit-builder --save-dev
+$ grunt
+```
 
 The first time will be slow because grunt will download precompiled nw binaries for all supported
-platforms, which will be stored in **dist/cache/**. From now you can compile for mac + linux + win
-with a simple `grunt` command. The compiled binaries will be stored in **dist/releases/**.
+platforms, which will be stored in *dist/cache/*. From now you can compile for mac + linux + win
+with a simple `grunt` command. The compiled binaries will be stored in *dist/releases/*.
 
 Congratulations! You now know how to use yet another weird stack born out of the HTML5 craze that
-isn't guaranteed to still be alive the next year (or even next month). For extra credit, use it to
-piss off long-time UNIX Philosophy believers. The [suckless][7] community would be a good place to
-start. ;)
+isn't guaranteed to still be alive the next year (or even next month).
 
 [1]: https://github.com/rogerwang/node-webkit
 [2]: https://github.com/Anonyfox/node-webkit-hipster-seed
diff --git a/node-webkit/index.html b/node-webkit/index.html
new file mode 100644
index 0000000..8317076
--- /dev/null
+++ b/node-webkit/index.html
@@ -0,0 +1,239 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Setting up your development environment for a node-webkit project | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2014-05-01">
+        Thursday, 01 May 2014
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Setting up your development environment for a node-webkit project</h1>
+<p><img alt="" src="nw_xp.png"></p>
+<p><a href="https://github.com/rogerwang/node-webkit">Node-webkit</a> lets you write cross-platform (Mac + Linux + Winbloze) desktop applications using
+HTML5 and nodejs. That’s a fancy way of saying “a webkit wrapper that also gives you filesystem
+access, disables same-origin policy and has nodejs embedded”. It’s the lazy web developers’ dream
+come true. Who needs Qt, GTK, or any other legitimate battle-tested, performant cross-platform GUI
+toolkit when you can slap in Bootstrap/Foundation with some hip JavaScript framework instead?</p>
+<p>Joking aside, this is an interesting take on desktop development. Firefox OS is on the horizon, and
+even Ubuntu is pushing the “HTML5 apps as first-class citizens” paradigm. Why not give it a try?</p>
+<p>If you already have experience in client-side web development tools, you can just clone
+<a href="https://github.com/Anonyfox/node-webkit-hipster-seed">node-webkit-hipster-seed</a> and digest the code. That’s a project skeleton that integrates all
+kinds of stuff: Jade/Coffescript/LESS automatic compiler, node-webkit, grunt tasks…</p>
+<p>This tutorial is like a stripped down version of that. In the end we’ll have:</p>
+<ul>
+<li>
+Automatic app reload when source code changes with <code>livereload</code>
+</li>
+<li>
+Single command to build binaries for mac + linux + win with <code>grunt</code>
+</li>
+<li>
+That’s it, really
+</li>
+</ul>
+<section id="Install-node-webkit-on-your-machine">
+<h2>Install node-webkit on your machine</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
+</code></pre>
+<p>If you’re on Ubuntu or some other repo and you get some error about <code>libudev.so.0</code>, <a href="http://www.exponential.io/blog/install-node-webkit-on-ubuntu-linux">read this</a>
+for a hotfix.</p>
+<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>
+<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/
+│  ├── css/
+│  ├── js/
+│  ├── index.html
+│  └── package.json
+└── dist/
+</code></pre>
+<p><strong>package.json</strong> stores the information that <code>nw</code> requires. Its content goes like this:</p>
+<pre><code class="language-json">{
+    "name": "your-project-name",
+    "version": "0.0.1",
+    "main": "index.html",
+    "window": {
+        "toolbar": true
+    }
+}
+</code></pre>
+<p><code>window.toolbar</code> is <code>true</code> by default so we don’t actually need it. Switch it to <code>false</code> if you
+want to hide the browser-like address bar.</p>
+<p>To embed css/js files, don’t use absolute urls. Use relative ones like this:</p>
+<pre><code class="language-html">&lt;link href="css/style.css" rel="stylesheet"&gt;
+</code></pre>
+<p>There’s no point in using CDNs because we’re distributing the whole app with its assets as a
+one-time download. Things like automatic css/js minification or concatination aren’t needed either.
+For simplicity’s sake, we’ll just download a minified version of whatever js/css library that we
+need and stuff them into <strong>css</strong> or <strong>js</strong> dir. For example: (<strong>dist</strong> directory omitted)</p>
+<pre><code>app/
+├── css
+│   └── bootstrap.min.css
+├── js
+│   ├── bootstrap.min.js
+│   ├── knockout.min.js
+│   ├── jquery.min.js
+│   └── app.js
+├── index.html
+└── package.json
+</code></pre>
+<p>If you’re experienced in front-end web development tools, feel free to go wild with bower,
+grunt/gulp/whatever. Again, check out the <a href="https://github.com/Anonyfox/node-webkit-hipster-seed">node-webkit-hipster-seed</a> project if you know what
+you’re doing.</p>
+<p>You can now test run your app with the <code>nw &lt;directory&gt;</code> command. In our case: <code>nw app</code>. Notice the
+weird url, which is why we can’t use absolute urls in the first place:</p>
+<pre><code>file:///home/nhanb/Dropbox/small_projects/ajmg-nw/app/index.html
+</code></pre>
+<p>Once you’ve packaged your app to a single executable, the url will be something like this:</p>
+<pre><code>file:///tmp/.org.chromium.Chromium.IJWqkq/index.html
+</code></pre>
+<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>
+<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>
+<p>But then, the true strength of <code>node-webkit</code> is the ability to use nodejs modules, which Google
+Chrome certainly doesn’t offer. Therefore, the only way to check out how the app really works is
+using <code>nw app</code>.</p>
+<p>We’ll use <code>livereload</code> to make automatic reloading possible. The idea is quite simple: we fire off
+a <code>livereload</code> daemon that watches for any change in our <strong>app/</strong> directory. In our app, we embed a
+certain piece of javascript that connects to that <code>livereload</code> daemon and refreshes the page
+whenever a “change” event is broadcast.</p>
+<p>There are many <code>livereload</code> daemon implementations. Considering the fact that most of us developers
+have python and pip installed, let’s go with the <code>livereload</code> pip package (it’s only compatible
+with python2, by the way). If you’re on Ubuntu and don’t know what I’m talking about:</p>
+<pre><code class="language-sh">$ sudo apt-get install python-pip
+$ sudo pip install livereload
+</code></pre>
+<p>There are ruby/javascript implementations too. Google them if you prefer those things.</p>
+<p>Either way, we can now fire off a livereload server:</p>
+<pre><code class="language-sh"># I don't know why but seems like the python implementation doesn't work
+# when I type `livereload app`. Weird.
+$ cd app
+$ livereload .
+</code></pre>
+<p>Now how do we inject the livereload javascript? On Google Chrome there is an official livereload
+plugin, but we’re using node-webkit so that’s not possible. No problem! The <code>livereload</code> daemon we
+fired off earlier is actually a web server which also serves the necessary livereload client
+JavaScript snippet too. Simply embed it to your <strong>index.html</strong>:</p>
+<pre><code class="language-html">&lt;script src="http://localhost:35729/livereload.js"&gt;&lt;/script&gt;
+</code></pre>
+<p>Fire off the app with <code>nw app</code> again and you’ll have automatic reloading. Cool eh?</p>
+<p>Another problem: we only want livereload in our development version, not in the released app. Let’s
+modify our javascript snippet in <strong>index.html</strong> to only load livereload when a certain environment
+variable is set to <code>1</code>:</p>
+<pre><code class="language-html">&lt;script&gt;
+// Load livereload if in dev environment
+if (process.env.NW_DEV_MY_AWESOME_PROJECT == 1) {
+    var script = document.createElement('script');
+    script.type = 'text/javascript';
+    script.src = 'http://localhost:35729/livereload.js';
+    document.body.appendChild(script);
+}
+&lt;/script&gt;
+</code></pre>
+<p>Now to start the app:</p>
+<pre><code class="language-sh">$ export NW_DEV_MY_AWESOME_PROJECT=1
+$ nw app
+</code></pre>
+</section>
+<section id="Simple-cross-platform-build-command">
+<h2>Simple cross-platform build command</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
+<a href="https://github.com/mllrsohn/grunt-node-webkit-builder">grunt-node-webkit-builder</a>. Again, for newcomers using Ubuntu:</p>
+<pre><code class="language-sh">$ sudo add-apt-repository ppa:chris-lea/node.js
+$ sudo apt-get update
+$ sudo apt-get install python-software-properties python g++ make nodejs
+$ sudo npm install -g grunt-cli
+$ # cd to your project root (the one containing app/ and dist/)
+</code></pre>
+<p>Create <code>package.json</code> and <code>Gruntfile.js</code> in project root:</p>
+<pre><code>├── app/
+│   ├── css/
+│   ├── js/
+│   ├── index.html
+│   └── package.json
+├── dist/
+├── Gruntfile.js
+└── package.json
+</code></pre>
+<p><strong>package.json</strong>:</p>
+<pre><code>{
+    "name": "whatever",
+    "version": "0.0.1",
+    "description": "Whatever",
+    "author": "Bui Thanh Nhan"
+}
+</code></pre>
+<p><strong>Gruntfile.js</strong>:</p>
+<pre><code class="language-javascript">module.exports = function(grunt) {
+    grunt.initConfig({
+        pkg: grunt.file.readJSON('package.json'),
+        nodewebkit: {
+            options: {
+                build_dir: './dist',
+                // choose what platforms to compile for here
+                mac: true,
+                win: true,
+                linux32: true,
+                linux64: true
+            },
+            src: ['./app/**/*']
+        }
+    })
+
+    grunt.loadNpmTasks('grunt-node-webkit-builder');
+    grunt.registerTask('default', ['nodewebkit']);
+};
+</code></pre>
+<p>Then:</p>
+<pre><code class="language-sh">$ npm install grunt grunt-node-webkit-builder --save-dev
+$ grunt
+</code></pre>
+<p>The first time will be slow because grunt will download precompiled nw binaries for all supported
+platforms, which will be stored in <strong>dist/cache/</strong>. From now you can compile for mac + linux + win
+with a simple <code>grunt</code> command. The compiled binaries will be stored in <strong>dist/releases/</strong>.</p>
+<p>Congratulations! You now know how to use yet another weird stack born out of the HTML5 craze that
+isn’t guaranteed to still be alive the next year (or even next month).</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/nw_xp.png b/node-webkit/nw_xp.png
similarity index 100%
rename from content/images/nw_xp.png
rename to node-webkit/nw_xp.png
diff --git a/notes/index.html b/notes/index.html
index 5d2ad25..5bba860 100644
--- a/notes/index.html
+++ b/notes/index.html
@@ -68,7 +68,7 @@ <h2>SRE</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/pathogen-vs-vundle/index.dj b/pathogen-vs-vundle/index.dj
index abe0f9f..6ecfd88 100644
--- a/pathogen-vs-vundle/index.dj
+++ b/pathogen-vs-vundle/index.dj
@@ -1,11 +1,6 @@
-date: 2013-05-13 12:00
-title: Modern vim plugin management: Pathogen vs Vundle
-slug: modern-vim-plugin-management-pathogen-vs-vundle
-lang: en
-category: tutorials
-tags: vim
-summary: Pimp your vim with little effort.
-
+Title: Modern vim plugin management: Pathogen vs Vundle
+PostedAt: 2013-05-13 12:00
+---
 
 For the impatient ones: Vundle is better than pathogen, use it.
 
@@ -24,12 +19,13 @@ To make this possible, by default vim looks for files in your home folder (which
 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.
 
-You will probably want to move this file into your ~/.vim folder to be able to manage everything
+You will probably want to move this file into your \~/.vim folder to be able to manage everything
 inside 1 folder. I will create `~/.vim/vimrc` then create a symlink pointing to it. Open a
 terminal and type:
 
-    :::bash
-    ln -s ~/.vim/vimrc ~/.vimrc
+```
+ln -s ~/.vim/vimrc ~/.vimrc
+```
 
 ## ~/.vim (directory)
 
@@ -42,61 +38,64 @@ This should contain a bunch of subdirectories. Some examples:
 - doc
 
 Each of these directories serves a particular purpose: `colors` contains colorschemes, `syntax`
-lets you add new rules for syntax highlighting, `doc` contains documentation...  
+lets you add new rules for syntax highlighting, `doc` contains documentation...
 A plugin will typically put its files into more than one directory here. For example, here is
 a plugin called [tagbar](https://github.com/majutsushi/tagbar), and I've installed it by
 copying its content into my `~/.vim` folder:
 
-    :::bash
-    ~/.vim
-    ├── autoload
-    │   └── tagbar.vim
-    ├── doc
-    │   ├── tagbar.txt
-    │   └── tags
-    ├── plugin
-    │   └── tagbar.vim
-    ├── README
-    └── syntax
-        └── tagbar.vim
+```
+~/.vim
+├── autoload
+│   └── tagbar.vim
+├── doc
+│   ├── tagbar.txt
+│   └── tags
+├── plugin
+│   └── tagbar.vim
+├── README
+└── syntax
+    └── tagbar.vim
+```
 
 Everything looks good. Just copy and paste the whole thing, nice and simple. How about adding a
 decent colorscheme? Let's install [solarized](https://github.com/altercation/vim-colors-solarized):
 
-    :::bash
-    ├── autoload
-    │   └── togglebg.vim
-    ├── bitmaps
-    │   └── togglebg.png
-    ├── colors
-    │   └── solarized.vim
-    ├── doc
-    │   ├── solarized.txt
-    │   └── tags
-    └── README.mkd
+```
+├── autoload
+│   └── togglebg.vim
+├── bitmaps
+│   └── togglebg.png
+├── colors
+│   └── solarized.vim
+├── doc
+│   ├── solarized.txt
+│   └── tags
+└── README.mkd
+```
 
 Wait, `doc/tags` is already there. Ok, no problem! Let's just copy the content of solarized's
 tags file and paste it into the existing one. Now we have:
 
-    :::bash
-    ~/.vim
-    ├── autoload
-    │   ├── tagbar.vim
-    │   └── togglebg.vim
-    ├── bitmaps
-    │   └── togglebg.png
-    ├── colors
-    │   └── solarized.vim
-    ├── doc
-    │   ├── solarized.txt
-    │   ├── tagbar.txt
-    │   └── tags
-    ├── plugin
-    │   └── tagbar.vim
-    ├── README
-    ├── README.mkd
-    └── syntax
-        └── tagbar.vim
+```
+~/.vim
+├── autoload
+│   ├── tagbar.vim
+│   └── togglebg.vim
+├── bitmaps
+│   └── togglebg.png
+├── colors
+│   └── solarized.vim
+├── doc
+│   ├── solarized.txt
+│   ├── tagbar.txt
+│   └── tags
+├── plugin
+│   └── tagbar.vim
+├── README
+├── README.mkd
+└── syntax
+    └── tagbar.vim
+```
 
 Now what if you you decide that solarized sucks and want to get rid of it? Good luck finding
 which file belongs to which plugin. Oh, don't forget the merged `doc/tags` file!
@@ -109,48 +108,51 @@ The legendary Tim Pope came up with a genius solution:
 [pathogen](https://github.com/tpope/vim-pathogen).
 Now let's install it like any regular plugin (I've omitted the README):
 
-    :::bash
-    ~/.vim
-    └── autoload
-        └── pathogen.vim
+```
+~/.vim
+└── autoload
+    └── pathogen.vim
+```
 
 Put this at the beginning of your `~/.vimrc`:
 
-    :::vim
-    execute pathogen#infect()
+```vim
+execute pathogen#infect()
+```
 
 Create this directory: `~/.vim/bundle`. To install tagbar and solarized, just create their own
 directories here:
 
-    :::bash
-    path
-    ├── autoload
-    │   └── pathogen.vim
-    └── bundle
-        ├── tagbar
-        │   ├── autoload
-        │   │   └── tagbar.vim
-        │   ├── doc
-        │   │   ├── tagbar.txt
-        │   │   └── tags
-        │   ├── plugin
-        │   │   └── tagbar.vim
-        │   ├── README
-        │   └── syntax
-        │       └── tagbar.vim
-        └── vim-colors-solarized
-            ├── autoload
-            │   └── togglebg.vim
-            ├── bitmaps
-            │   └── togglebg.png
-            ├── colors
-            │   └── solarized.vim
-            ├── doc
-            │   ├── solarized.txt
-            │   └── tags
-            └── README.mkd
-
-What Pathogen does is that it adds every directory inside `bundle` into vim's "runtimepath".
+```
+path
+├── autoload
+│   └── pathogen.vim
+└── bundle
+    ├── tagbar
+    │   ├── autoload
+    │   │   └── tagbar.vim
+    │   ├── doc
+    │   │   ├── tagbar.txt
+    │   │   └── tags
+    │   ├── plugin
+    │   │   └── tagbar.vim
+    │   ├── README
+    │   └── syntax
+    │       └── tagbar.vim
+    └── vim-colors-solarized
+        ├── autoload
+        │   └── togglebg.vim
+        ├── bitmaps
+        │   └── togglebg.png
+        ├── colors
+        │   └── solarized.vim
+        ├── doc
+        │   ├── solarized.txt
+        │   └── tags
+        └── README.mkd
+```
+
+Pathogen adds every directory inside `bundle` into vim's "runtimepath".
 It means that each folder here can be considered a new `.vim` folder where vim looks for
 appropriate configuration files. The plugins are now isolated so removing or updating them
 becomes trivial: just remove or update its own directory.
@@ -162,9 +164,10 @@ haven't created a [Github](https://github.com) account, do it now. Create an emp
 with any name you want (mine is `.vim`). Don't commit yet. Create a file: `~/.vim/.gitignore`,
 add these lines to its content:
 
-    :::bash
-    bundle/
-    .netrwhist
+```
+bundle/
+.netrwhist
+```
 
 .netrwhist is a local file generated by vim that is better off ignored. We also ignore bundle
 directory because the plugins will be included as git submodules (google *git submodule*
@@ -174,51 +177,55 @@ plugins again with git.
 Git init, commit and push to your github repo: (on the *git remote add...* line, replace `nhanb`
 with your github username, `.vim` with your repo name)
 
-    :::bash
-    cd ~/.vim
-    git init
-    git add .
-    git commit -m 'init'
+```
+cd ~/.vim
+git init
+git add .
+git commit -m 'init'
 
-    git remote add origin https://github.com/nhanb/.vim.git
-    git push -u origin master
+git remote add origin https://github.com/nhanb/.vim.git
+git push -u origin master
+```
 
 Everytime you edit anything in your .vim directory, remember to commit the changes and push to
 github:
 
-    :::bash
-    git add . 
-    git commit -m 'some message here'
-    git push
+```
+git add .
+git commit -m 'some message here'
+git push
+```
 
 If you want to install a plugin, see if it has a git repo (9 out of 10 times it has a
 github repo). Find its git url and add to your .vim as a submodule:
 
-    :::bash
-    cd ~/.vim
-    git add submodule https://github.com/majutsushi/tagbar.git bundle/tagbar
-    git add submodule https://github.com/altercation/vim-colors-solarized.git bundle/solarized
-    git submodule update --init
-    git submodule foreach git pull origin master
+```
+cd ~/.vim
+git add submodule https://github.com/majutsushi/tagbar.git bundle/tagbar
+git add submodule https://github.com/altercation/vim-colors-solarized.git bundle/solarized
+git submodule update --init
+git submodule foreach git pull origin master
+```
 
 When you need to update your plugins, just run the last line to make git pull updates for all
-plugins. 
+plugins.
 
 Here's the awesome part: when you're using a whole new computer and want to get all your vim settings
 from the cloud, simply clone your github repo, make a symlink for .vimrc and pull all plugins:
 
-    :::bash
-    cd ~
-    git clone https://github.com/nhanb/.vim.git .vim
-    ln -s ~/.vim/vimrc ~/.vimrc
-    cd .vim
-    git submodule update --init && git submodule foreach git pull origin master
+```
+cd ~
+git clone https://github.com/nhanb/.vim.git .vim
+ln -s ~/.vim/vimrc ~/.vimrc
+cd .vim
+git submodule update --init && git submodule foreach git pull origin master
+```
 
 Now you must be really excited, no? Git does everything for you: upload/download, add plugins,
 update plugins *and* remove plugins... There must be some simple git command to remove a
 submodule, right?
 
-**NO**. Sadly, no. To remove a git submodule, you'll need to manually edit 2 git files and
+*NO*. Sadly, no. To remove a git submodule, you'll need to manually edit 2 git files and
 remove the folder by hand. See
 [this Stackoverflow question](http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule)
 for detailed instructions.
@@ -228,38 +235,40 @@ for detailed instructions.
 This time let's start fresh: remove all submodules and pathogen. Your bundle folder should be
 now empty. Clone [Vundle](https://github.com/gmarik/vundle):
 
-    :::bash
-    git clone https://github.com/gmarik/vundle.git ~/.vim/bundle/vundle
+```sh
+git clone https://github.com/gmarik/vundle.git ~/.vim/bundle/vundle
+```
 
 Put this in your .vimrc (preferably at the beginning):
 
-    :::vim
-    set nocompatible               " be iMproved
-    filetype off                   " required!
+```vim
+set nocompatible               " be iMproved
+filetype off                   " required!
 
-    set rtp+=~/.vim/bundle/vundle/
-    call vundle#rc()
+set rtp+=~/.vim/bundle/vundle/
+call vundle#rc()
 
-    " let Vundle manage Vundle
-    " required! 
-    Bundle 'gmarik/vundle'
+" let Vundle manage Vundle
+" required! 
+Bundle 'gmarik/vundle'
 
-    " My Bundles here:
-    "
-    " original repos on github
-    Bundle 'majutsushi/tagbar'
-    Bundle 'altercation/vim-colors-solarized'
+" My Bundles here:
+"
+" original repos on github
+Bundle 'majutsushi/tagbar'
+Bundle 'altercation/vim-colors-solarized'
 
-    " Github repos of the user 'vim-scripts'
-    " => can omit the username part
-    Bundle 'L9'
-    Bundle 'FuzzyFinder'
+" Github repos of the user 'vim-scripts'
+" => can omit the username part
+Bundle 'L9'
+Bundle 'FuzzyFinder'
 
-    " non github repos
-    Bundle 'git://git.wincent.com/command-t.git'
-    " ...
+" non github repos
+Bundle 'git://git.wincent.com/command-t.git'
+" ...
 
-    filetype plugin indent on     " required!
+filetype plugin indent on     " required!
+```
 
 Relaunch vim, run `:BundleInstall` to install the "bundles" you listed in .vimrc. When you want
 to update them, `:BundleUpdate`. To remove a plugin, just delete its line in your .vimrc file
diff --git a/pathogen-vs-vundle/index.html b/pathogen-vs-vundle/index.html
new file mode 100644
index 0000000..90eda2b
--- /dev/null
+++ b/pathogen-vs-vundle/index.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Modern vim plugin management: Pathogen vs Vundle | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2013-05-13">
+        Monday, 13 May 2013
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Modern vim plugin management: Pathogen vs Vundle</h1>
+<p>For the impatient ones: Vundle is better than pathogen, use it.</p>
+<p>This post will explain how vim plugins work and how to easily manage your plugins with
+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>
+<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>
+<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
+inside 1 folder. I will create <code>~/.vim/vimrc</code> then create a symlink pointing to it. Open a
+terminal and type:</p>
+<pre><code>ln -s ~/.vim/vimrc ~/.vimrc
+</code></pre>
+</section>
+<section id="vim-directory">
+<h2>~/.vim (directory)</h2>
+<p>This should contain a bunch of subdirectories. Some examples:</p>
+<ul>
+<li>
+autoload
+</li>
+<li>
+ftplugin
+</li>
+<li>
+colors
+</li>
+<li>
+syntax
+</li>
+<li>
+doc
+</li>
+</ul>
+<p>Each of these directories serves a particular purpose: <code>colors</code> contains colorschemes, <code>syntax</code>
+lets you add new rules for syntax highlighting, <code>doc</code> contains documentation…
+A plugin will typically put its files into more than one directory here. For example, here is
+a plugin called <a href="https://github.com/majutsushi/tagbar">tagbar</a>, and I’ve installed it by
+copying its content into my <code>~/.vim</code> folder:</p>
+<pre><code>~/.vim
+├── autoload
+│   └── tagbar.vim
+├── doc
+│   ├── tagbar.txt
+│   └── tags
+├── plugin
+│   └── tagbar.vim
+├── README
+└── syntax
+    └── tagbar.vim
+</code></pre>
+<p>Everything looks good. Just copy and paste the whole thing, nice and simple. How about adding a
+decent colorscheme? Let’s install <a href="https://github.com/altercation/vim-colors-solarized">solarized</a>:</p>
+<pre><code>├── autoload
+│   └── togglebg.vim
+├── bitmaps
+│   └── togglebg.png
+├── colors
+│   └── solarized.vim
+├── doc
+│   ├── solarized.txt
+│   └── tags
+└── README.mkd
+</code></pre>
+<p>Wait, <code>doc/tags</code> is already there. Ok, no problem! Let’s just copy the content of solarized’s
+tags file and paste it into the existing one. Now we have:</p>
+<pre><code>~/.vim
+├── autoload
+│   ├── tagbar.vim
+│   └── togglebg.vim
+├── bitmaps
+│   └── togglebg.png
+├── colors
+│   └── solarized.vim
+├── doc
+│   ├── solarized.txt
+│   ├── tagbar.txt
+│   └── tags
+├── plugin
+│   └── tagbar.vim
+├── README
+├── README.mkd
+└── syntax
+    └── tagbar.vim
+</code></pre>
+<p>Now what if you you decide that solarized sucks and want to get rid of it? Good luck finding
+which file belongs to which plugin. Oh, don’t forget the merged <code>doc/tags</code> file!
+Now imagine you have 20-30 plugins installed (which is normal, by the way). It’s not a
+pretty sight now, is it?</p>
+</section>
+<section id="Pathogen-to-the-rescue">
+<h2>Pathogen to the rescue!</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>
+<pre><code>~/.vim
+└── autoload
+    └── pathogen.vim
+</code></pre>
+<p>Put this at the beginning of your <code>~/.vimrc</code>:</p>
+<pre><code class="language-vim">execute pathogen#infect()
+</code></pre>
+<p>Create this directory: <code>~/.vim/bundle</code>. To install tagbar and solarized, just create their own
+directories here:</p>
+<pre><code>path
+├── autoload
+│   └── pathogen.vim
+└── bundle
+    ├── tagbar
+    │   ├── autoload
+    │   │   └── tagbar.vim
+    │   ├── doc
+    │   │   ├── tagbar.txt
+    │   │   └── tags
+    │   ├── plugin
+    │   │   └── tagbar.vim
+    │   ├── README
+    │   └── syntax
+    │       └── tagbar.vim
+    └── vim-colors-solarized
+        ├── autoload
+        │   └── togglebg.vim
+        ├── bitmaps
+        │   └── togglebg.png
+        ├── colors
+        │   └── solarized.vim
+        ├── doc
+        │   ├── solarized.txt
+        │   └── tags
+        └── README.mkd
+</code></pre>
+<p>Pathogen adds every directory inside <code>bundle</code> into vim’s “runtimepath”.
+It means that each folder here can be considered a new <code>.vim</code> folder where vim looks for
+appropriate configuration files. The plugins are now isolated so removing or updating them
+becomes trivial: just remove or update its own directory.</p>
+</section>
+<section id="Pathogen-Git">
+<h2>Pathogen + Git</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>,
+add these lines to its content:</p>
+<pre><code>bundle/
+.netrwhist
+</code></pre>
+<p>.netrwhist is a local file generated by vim that is better off ignored. We also ignore bundle
+directory because the plugins will be included as git submodules (google <strong>git submodule</strong>
+for details). Remember to delete everything inside <code>bundle/</code>, because we will install the
+plugins again with git.</p>
+<p>Git init, commit and push to your github repo: (on the <strong>git remote add…</strong> line, replace <code>nhanb</code>
+with your github username, <code>.vim</code> with your repo name)</p>
+<pre><code>cd ~/.vim
+git init
+git add .
+git commit -m 'init'
+
+git remote add origin https://github.com/nhanb/.vim.git
+git push -u origin master
+</code></pre>
+<p>Everytime you edit anything in your .vim directory, remember to commit the changes and push to
+github:</p>
+<pre><code>git add .
+git commit -m 'some message here'
+git push
+</code></pre>
+<p>If you want to install a plugin, see if it has a git repo (9 out of 10 times it has a
+github repo). Find its git url and add to your .vim as a submodule:</p>
+<pre><code>cd ~/.vim
+git add submodule https://github.com/majutsushi/tagbar.git bundle/tagbar
+git add submodule https://github.com/altercation/vim-colors-solarized.git bundle/solarized
+git submodule update --init
+git submodule foreach git pull origin master
+</code></pre>
+<p>When you need to update your plugins, just run the last line to make git pull updates for all
+plugins.</p>
+<p>Here’s the awesome part: when you’re using a whole new computer and want to get all your vim settings
+from the cloud, simply clone your github repo, make a symlink for .vimrc and pull all plugins:</p>
+<pre><code>cd ~
+git clone https://github.com/nhanb/.vim.git .vim
+ln -s ~/.vim/vimrc ~/.vimrc
+cd .vim
+git submodule update --init &amp;&amp; git submodule foreach git pull origin master
+</code></pre>
+<p>Now you must be really excited, no? Git does everything for you: upload/download, add plugins,
+update plugins <strong>and</strong> remove plugins… There must be some simple git command to remove a
+submodule, right?</p>
+<p><strong>NO</strong>. Sadly, no. To remove a git submodule, you’ll need to manually edit 2 git files and
+remove the folder by hand. See
+<a href="http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule">this Stackoverflow question</a>
+for detailed instructions.</p>
+</section>
+<section id="Vundle-the-new-cool-kid">
+<h2>Vundle, the new cool kid</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
+</code></pre>
+<p>Put this in your .vimrc (preferably at the beginning):</p>
+<pre><code class="language-vim">set nocompatible               " be iMproved
+filetype off                   " required!
+
+set rtp+=~/.vim/bundle/vundle/
+call vundle#rc()
+
+" let Vundle manage Vundle
+" required! 
+Bundle 'gmarik/vundle'
+
+" My Bundles here:
+"
+" original repos on github
+Bundle 'majutsushi/tagbar'
+Bundle 'altercation/vim-colors-solarized'
+
+" Github repos of the user 'vim-scripts'
+" =&gt; can omit the username part
+Bundle 'L9'
+Bundle 'FuzzyFinder'
+
+" non github repos
+Bundle 'git://git.wincent.com/command-t.git'
+" ...
+
+filetype plugin indent on     " required!
+</code></pre>
+<p>Relaunch vim, run <code>:BundleInstall</code> to install the “bundles” you listed in .vimrc. When you want
+to update them, <code>:BundleUpdate</code>. To remove a plugin, just delete its line in your .vimrc file
+then relaunch vim and run <code>:BundleClean</code> to remove its folder inside ~/.vim/bundle/</p>
+<p>Vundle follows Pathogen’s approach: putting plugins in their separate directories. However,
+it also takes care of the git stuff for us too! Note that by default it uses <code>git clone</code>, not
+<code>git add submodule</code> to add plugins. If you’re using Windows, there’s Vundle for Windows too,
+though I’ve never tried it.</p>
+<p>That’s it, happy coding! Feel free to leave your comments if there’s anything wrong/unclear here.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/petition-fraud/index.dj b/petition-fraud/index.dj
index 9056065..4d63e99 100644
--- a/petition-fraud/index.dj
+++ b/petition-fraud/index.dj
@@ -1,10 +1,10 @@
 Title: I did NOT sign that online petition!
-Slug: i-did-not-sign-that-rmit-sc-petition
-Date: 2016-03-12 01:13
+PostedAt: 2016-03-12 01:13
+---
 
 This evening I received a rather strange email:
 
-![](/images/rmitsc_01_wtf.png)
+![](rmitsc_01_wtf.png)
 
 Um... I don't remember signing any petition recently (or ever, for that matter)?
 
@@ -15,10 +15,10 @@ university's Student Council. Said petition was apparently started by some Ms. T
 Student Council's vice president. Well... yay for free speech, I guess? Anyway, my name was really
 among the signers list:
 
-![](/images/rmitsc_02_names.png)
+![](rmitsc_02_names.png)
 
-How did this happen? Well, turns out **iPetition does not require email confirmation upon
-signing**, so anyone can effectively enter any email and name they want and the stupid website will
+How did this happen? Well, turns out *iPetition does not require email confirmation upon
+signing*, so anyone can effectively enter any email and name they want and the stupid website will
 happily accept that as an absolutely definitely most positively legit supporter of your cause. Cool
 huh?
 
@@ -45,7 +45,7 @@ Hell, let's throw in some of my own random thoughts to make this more like a blo
 
 - The number of signers keep going up steadily, but slowly. Maybe our friendly neighborhood
   signerman is doing it all by hand instead of a script? If it's the former... let's say I do
-  admire the dedication <strike>and abhor the absolute stupidity</strike>.
+  admire the dedication {-and abhor the absolute stupidity-}.
 
 - Is it normal for a student council to be this full of drama?
 
diff --git a/petition-fraud/index.html b/petition-fraud/index.html
new file mode 100644
index 0000000..cb0ef90
--- /dev/null
+++ b/petition-fraud/index.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>I did NOT sign that online petition! | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2016-03-12">
+        Saturday, 12 Mar 2016
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>I did NOT sign that online petition!</h1>
+<p>This evening I received a rather strange email:</p>
+<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>
+<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
+among the signers list:</p>
+<p><img alt="" src="rmitsc_02_names.png"></p>
+<p>How did this happen? Well, turns out <strong>iPetition does not require email confirmation upon
+signing</strong>, so anyone can effectively enter any email and name they want and the stupid website will
+happily accept that as an absolutely definitely most positively legit supporter of your cause. Cool
+huh?</p>
+<p>Upon further inspection, almost all of the signer names are in one same format: the one that RMIT
+uses for its student names. So apparently a very motivated supporter of Ms. Mẫn’s… interesting
+campaign has been helpful enough to go through RMIT students’ IDs and names and sign us up, without
+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>
+<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
+know.</p>
+<p>Hell, let’s throw in some of my own random thoughts to make this more like a blog post:</p>
+<ul>
+<li>
+<p>What’s with these petition websites? Have people actually achieved anything using these? Even if
+a petition website does send confirmation emails, what’s stopping me from using trash addresses?
+The signers’ email addresses are not displayed anyway so president@rmit.edu.vn won’t be too
+different from lol0042@spam.me now, will it? If you’re an official co-leader of something
+official who wants to do something official about it, maybe try a more, I don’t know, official
+channel?</p>
+</li>
+<li>
+<p>The number of signers keep going up steadily, but slowly. Maybe our friendly neighborhood
+signerman is doing it all by hand instead of a script? If it’s the former… let’s say I do
+admire the dedication <del>and abhor the absolute stupidity</del>.</p>
+</li>
+<li>
+<p>Is it normal for a student council to be this full of drama?</p>
+</li>
+</ul>
+<p>That concludes my mostly pointless blog post. Hopefully I’ll come up with something actually worth
+sharing soon. Until then, have an awesome weekend!</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/rmitsc_01_wtf.png b/petition-fraud/rmitsc_01_wtf.png
similarity index 100%
rename from content/images/rmitsc_01_wtf.png
rename to petition-fraud/rmitsc_01_wtf.png
diff --git a/content/images/rmitsc_02_names.png b/petition-fraud/rmitsc_02_names.png
similarity index 100%
rename from content/images/rmitsc_02_names.png
rename to petition-fraud/rmitsc_02_names.png
diff --git a/pippable-webapp/index.dj b/pippable-webapp/index.dj
index 5a691d7..9c0f968 100644
--- a/pippable-webapp/index.dj
+++ b/pippable-webapp/index.dj
@@ -1,6 +1,6 @@
 Title: I made my python webapp installable via pip
-Date: 2021-10-02 19:49
-Slug: i-made-my-python-webapp-pip-installable
+PostedAt: 2021-10-02 19:49
+---
 
 Running `pip3 install pytaku` now gives you all the tools you need [^1] [^2] to
 deploy [pytaku][3] - a hobby webapp of mine - on a fresh Debian 11 server:
@@ -112,9 +112,9 @@ easily write a couple of lines of Dockerfile for it by themselves is more
 valuable. Simple distribution is simple to deploy regardless of whether you're
 using docker, packer, ansible, pyinfra, podman, nomad, k8s, k3s, an
 impenetrable shell script some dude wrote 2 years ago who just left the company
-last month... or any combination of the above. The point is **you shouldn't be
+last month... or any combination of the above. The point is *you shouldn't be
 forced to use more heavyweight solutions just because the software is a pain in
-the butt to setup manually**.
+the butt to setup manually*.
 
 And other people _have_ been trying to make python application distribution
 simpler:
diff --git a/pippable-webapp/index.html b/pippable-webapp/index.html
new file mode 100644
index 0000000..8fa707e
--- /dev/null
+++ b/pippable-webapp/index.html
@@ -0,0 +1,193 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>I made my python webapp installable via pip | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2021-10-02">
+        Saturday, 02 Oct 2021
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>I made my python webapp installable via pip</h1>
+<p>Running <code>pip3 install pytaku</code> now gives you all the tools you need <a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> <a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> to
+deploy <a href="https://sr.ht/~nhanb/pytaku/">pytaku</a> - a hobby webapp of mine - on a fresh Debian 11 server:</p>
+<pre><code class="language-sh">pytaku-generate-config &gt; pytaku.conf.json  # generate config file
+pytaku-migrate  # generate initial sqlite3 db, or migrate to new version
+pytaku -w 5  # run main webapp using gunicorn on localhost:8000
+pytaku-scheduler  # daemon that executes scheduled background tasks
+
+# Optionally, run this to copy all static assets to a designated dir so your
+# web server (nginx/caddy/etc.) can serve them directly instead of through
+# the less performant gunicorn:
+pytaku-collect-static /var/www/pytaku
+</code></pre>
+<p>So how does that work? Let’s break it down.</p>
+<section id="The-pytaku-executables">
+<h2>The pytaku-* executables</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
+alongside my library is as simple as writing a few lines in my
+<a href="https://git.sr.ht/~nhanb/pytaku/tree/ff20e51f8c178bf981d80aa3737bf31a1059a506/item/pyproject.toml#L15-21">pyproject.toml</a> file:</p>
+<pre><code class="language-toml">[tool.poetry.scripts]
+pytaku = "pytaku:serve"
+pytaku-dev = "pytaku:dev"
+pytaku-migrate = "pytaku:migrate"
+pytaku-generate-config = "pytaku:generate_config"
+pytaku-scheduler = "pytaku:scheduler"
+pytaku-collect-static = "pytaku:collect_static"
+</code></pre>
+<p>The left hand side indicates the executable file name, while the right hand
+side declares which function to call. In my example, “pytaku:serve” points to
+the serve() function inside src/pytaku/__init__.py.</p>
+<p>Now that we have easy access to CLI entry points, let’s quickly go over
+<a href="https://git.sr.ht/~nhanb/pytaku/tree/ff20e51f8c178bf981d80aa3737bf31a1059a506/item/src/pytaku/__init__.py">how</a> each command works:</p>
+<ul>
+<li>
+<code>pytaku</code> and <code>pytaku-dev</code> simply exec gunicorn and flask respectively
+behind the scene.
+</li>
+<li>
+<code>pytaku-migrate</code> runs my bespoke migrator script (which is extremely
+primitive but hey it was a good learning experience).
+</li>
+<li>
+<code>pytaku-generate-config</code> uses <a href="https://github.com/lincolnloop/goodconf">goodconf</a> to generate a config template,
+pre-filling as many values as it can.
+</li>
+<li>
+<code>pytaku-scheduler</code> is just a dead simple single-threaded scheduler that I
+don’t recommend for any service that has more than a handful of users.
+</li>
+<li>
+<code>pytaku-collect-static</code> leverages importlib.resources.path to get the
+package’s installation path. From there it copies the bundled static assets
+to wherever you want your nginx to serve. It’s basically a simplified version
+of Django’s collectstatic command.
+</li>
+</ul>
+</section>
+<section id="But-why-bother">
+<h2>But why bother?</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
+pip-friendly. The “why” boils down to 2 points:</p>
+<ul>
+<li>
+You don’t want to reinvent package management. Let pip handle the minute
+details of packaging, distributing, versioning, etc. for you.
+</li>
+<li>
+You no longer need to run python from the source code’s path. In pytaku’s
+case, the working dir now only stores the sqlite database file and the
+json config file, i.e. purely data, completely separate from the source
+code.
+</li>
+</ul>
+<p>More broadly, the idea of simple distributing/deployment is, in my opinion,
+often overlooked these days. Fiddly deployment procedures are largely why
+Docker flourished: our industry just collectively gave up on self-contained
+software distribution and decided to ship a whole rootfs for each application
+process instead. Okay, I may be overreacting here, but I think it’s at least
+fair to say that if every webdev shop standardized on shipping Go binaries
+statically compiled with musl libc, we’d probably reach out for Docker less
+often. When I showed pytaku to a colleague of mine, his first question was
+essentially “Dockerfile when?”. Sure, Docker is neat and solves real problems,
+but how about we strive to avoid, or at least minimize, those problems in the
+first place? Remember, while container evangelists love harping on about
+negligible CPU overhead, they tend to gloss over the storage overhead:</p>
+<pre><code class="language-sh">$ docker image ls
+REPOSITORY   TAG        IMAGE ID       CREATED       SIZE
+python       3-alpine   bcf864391ba1   3 weeks ago   45.1MB
+python       3-slim     66f4843b721f   3 weeks ago   122MB
+</code></pre>
+<p>And the operational complexity overhead. Did you know that by default Docker
+<a href="https://www.jeffgeerling.com/blog/2020/be-careful-docker-might-be-exposing-ports-world">completely sidesteps your firewall</a>? That even if you specifically tell it
+to only listen to a port on localhost, it may or may not still expose it to the
+whole world? That this remains an <a href="https://github.com/moby/moby/issues/22054">open bug since 2016</a>? This isn’t one of
+those security bogeyman stories either, actual people have been <a href="https://blog.newsblur.com/2021/06/28/story-of-a-hacking/">bitten by
+it</a>. At this point cloud apologists would probably jump in and point out
+how this isn’t an issue if you’re running on GCP or AWS because they have
+another layer of firewall that locks down every port by default that you can
+setup on their totally usable web console or infrastructure-as-code it in your
+cloudformations or your terraformses or, actually, do you have a moment to talk
+about our lord and savior Cthulhubernetes–</p>
+<p>But I digress.</p>
+<p>I guess what I was trying to say is, throwing abstractions over complex
+procedures is simply shifting the costs elsewhere. Shipping your software in a
+Dockerfile is fine, but making your distribution so simple that people can
+easily write a couple of lines of Dockerfile for it by themselves is more
+valuable. Simple distribution is simple to deploy regardless of whether you’re
+using docker, packer, ansible, pyinfra, podman, nomad, k8s, k3s, an
+impenetrable shell script some dude wrote 2 years ago who just left the company
+last month… or any combination of the above. The point is <strong>you shouldn’t be
+forced to use more heavyweight solutions just because the software is a pain in
+the butt to setup manually</strong>.</p>
+<p>And other people <em>have</em> been trying to make python application distribution
+simpler:</p>
+<ul>
+<li>
+<a href="https://shiv.readthedocs.io/en/latest/">shiv</a> bundles everything but the python interpreter
+</li>
+<li>
+<a href="https://github.com/indygreg/PyOxidizer">PyOxidizer</a> bundles everything <em>including</em> the python interpreter
+</li>
+<li>
+<a href="https://nuitka.net/">nuika</a> actually compiles your python application into an executable,
+unlike PyInstaller which just generates a self-extracting archive.
+</li>
+</ul>
+<p>We’ll get there. Someday.</p>
+</section>
+<section role="doc-endnotes">
+<hr>
+<ol>
+<li id="fn1">
+<p>Well actually you still need to <code>apt install python3-apsw</code>, but that’s
+only because apsw <a href="https://rogerbinns.github.io/apsw/download.html#easy-install-pip-pypi">refuses</a> to provide a binary wheel on pypi. It can be
+replaced by the standard library sqlite3 module anyway - I only picked apsw
+because it exposes essentially the same API as the SQLite C library, which
+helped when I was learning to use SQLite properly for the first time.<a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
+</li>
+<li id="fn2">
+<p>Even with the above, pytaku still won’t run out of the box because it
+needs a <a href="https://github.com/nhanb/gae-proxy/">crappy proxy</a> in order to
+bypass mangasee’s strict cloudflare protection. I know it’s lame but pytaku
+is practically a web scraper project and there’s no way to make it work
+reliably without a proxy pool anyway. I hope this doesn’t distract you from
+the point of the article though.<a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
+</li>
+</ol>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/projects/index.html b/projects/index.html
index 0f428ef..7850620 100644
--- a/projects/index.html
+++ b/projects/index.html
@@ -78,7 +78,7 @@ <h2>Caophim</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/pyqt5/index.dj b/pyqt5/index.dj
index 1bd09ce..f8523a0 100644
--- a/pyqt5/index.dj
+++ b/pyqt5/index.dj
@@ -1,7 +1,6 @@
 Title: How to install PyQt5 on a virtualenv on Ubuntu 14.04
-Slug: how-to-install-pyqt5-on-virtualenv-on-ubuntu-14.04
-Date: 2015-02-14 22:33
-Category: tutorials
+PostedAt: 2015-02-14 22:33
+---
 
 The official way to install PyQt5 for development is to download and compile SIP + PyQt5 from
 source, which is painstakingly slow (compiling PyQt5 took like 10 minutes on my PC). If you're
@@ -10,9 +9,9 @@ Alhough it is doable, I prefer something faster.
 
 And yes, there is something faster. Today I came across a [Stack Overflow answer][2] that suggested
 a neat trick: installing PyQt globally, then copy the whole thing to your virtualenv
-**site-packages** directory. Here's how I did it on Ubuntu 14.04, python3.4 and PyQt5:
+*site-packages* directory. Here's how I did it on Ubuntu 14.04, python3.4 and PyQt5:
 
-```bash
+```sh
 # assuming you already have virtualenv & virtualenvwrapper installed
 
 # install pyqt5 globally
diff --git a/pyqt5/index.html b/pyqt5/index.html
new file mode 100644
index 0000000..e1f6e9c
--- /dev/null
+++ b/pyqt5/index.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>How to install PyQt5 on a virtualenv on Ubuntu 14.04 | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2015-02-14">
+        Saturday, 14 Feb 2015
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>How to install PyQt5 on a virtualenv on Ubuntu 14.04</h1>
+<p>The official way to install PyQt5 for development is to download and compile SIP + PyQt5 from
+source, which is painstakingly slow (compiling PyQt5 took like 10 minutes on my PC). If you’re
+<a href="https://michalcodes4life.wordpress.com/2014/03/16/pyqt5-python-3-3-in-virtualenv-on-ubuntu/">compiling it against a virtualenv</a>, rinse and repeat for each new virtualenv you create.
+Alhough it is doable, I prefer something faster.</p>
+<p>And yes, there is something faster. Today I came across a <a href="http://stackoverflow.com/a/1962076">Stack Overflow answer</a> that suggested
+a neat trick: installing PyQt globally, then copy the whole thing to your virtualenv
+<strong>site-packages</strong> directory. Here’s how I did it on Ubuntu 14.04, python3.4 and PyQt5:</p>
+<pre><code class="language-sh"># assuming you already have virtualenv &amp; virtualenvwrapper installed
+
+# install pyqt5 globally
+sudo apt-get install python3-pyqt5
+
+mkvirtualenv -p `which python3` cookies
+# (replace "cookies" with your actual virtualenv name, duh!)
+
+LIBDIR="$HOME/virtualenvs/cookies/lib/python3.4/site-packages"
+cp -r /usr/lib/python3/dist-packages/PyQt5 "$LIBDIR/PyQt5"
+cp /usr/lib/python3/dist-packages/sip.cpython-*.so "$LIBDIR/"
+</code></pre>
+<p>And you’re done with no compiling involved. Isn’t that neat? :)</p>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/pytaku/index.dj b/pytaku-old/index.dj
similarity index 78%
rename from pytaku/index.dj
rename to pytaku-old/index.dj
index 28697a7..9c3385c 100644
--- a/pytaku/index.dj
+++ b/pytaku-old/index.dj
@@ -1,15 +1,17 @@
 Title: Introducing Pytaku—the only online manga reader you'll ever need
-Date: 2015-01-02 21:19
-Category: side projects
-Tags: pytaku
-Thumb: images/pytaku_01_chapter_progress.png
+PostedAt: 2015-01-02 21:19
+Thumb: pytaku_01_chapter_progress.png
 
-> **Heads up from 2021!** This post describes a previous incarnation of Pytaku
-> which is no longer alive.  The new Pytaku is a slightly different thing which
-> is being (relatively) actively developed [on
-> sourcehut](https://sr.ht/~nhanb/pytaku/).
+---
 
-[Pytaku][pytaku] is an online manga reader that scrapes data from multiple Vietnamese and English
+*Heads up from 2021!* This post describes a previous incarnation of Pytaku
+which is no longer alive.  The new Pytaku is a slightly different thing which
+is being (relatively) actively developed [on
+sourcehut](https://sr.ht/~nhanb/pytaku/).
+
+---
+
+Pytaku is an online manga reader that scrapes data from multiple Vietnamese and English
 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:
 
@@ -61,14 +63,14 @@ chat room][3].
 
 ## Give it a spin
 
-[Click here][pytaku] to go to the app. Have fun! :)
+{-Click here to go to the app. Have fun! :)-} Update: this version of pytaku is
+no longer online.
 
 [1]: https://github.com/nhanb/pytaku-old-gae/
 [2]: https://github.com/nhanb/pytaku-old-gae/blob/master/README.markdown
 [3]: https://gitter.im/nhanb/pytaku
 [4]: https://github.com/nhanb/pytaku-old-gae/blob/master/frontend/languages/en.yaml
-[img1]: /images/pytaku_01_chapter_progress.png
-[img2]: /images/pytaku_02_bookmarked_series.png
-[img3]: /images/pytaku_03_vietnamese.png
-[pytaku]: https://pytaku.appspot.com
+[img1]: pytaku_01_chapter_progress.png
+[img2]: pytaku_02_bookmarked_series.png
+[img3]: pytaku_03_vietnamese.png
 [gpl]: https://www.gnu.org/licenses/quick-guide-gplv3.html
diff --git a/pytaku-old/index.html b/pytaku-old/index.html
new file mode 100644
index 0000000..bab4cf9
--- /dev/null
+++ b/pytaku-old/index.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Introducing Pytaku—the only online manga reader you&#39;ll ever need | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2015-01-02">
+        Friday, 02 Jan 2015
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Introducing Pytaku—the only online manga reader you&#39;ll ever need</h1>
+<p><strong>Heads up from 2021!</strong> This post describes a previous incarnation of Pytaku
+which is no longer alive.  The new Pytaku is a slightly different thing which
+is being (relatively) actively developed <a href="https://sr.ht/~nhanb/pytaku/">on
+sourcehut</a>.</p>
+<hr>
+<p>Pytaku is an online manga reader that scrapes data from multiple Vietnamese and English
+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>
+<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>
+<p>And it gets better: using state-of-the-art AJAX dark magic, even navigation between chapters is
+lightning fast. Loaded pages are cached, so pressing “Back” or “Forward” on your browser happens
+instantly.</p>
+</section>
+<section id="Keep-track-of-your-reading-progress-Automatically">
+<h2>Keep track of your reading progress. Automatically.</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>
+<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>
+<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>
+<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>
+<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>
+<p><del>Click here to go to the app. Have fun! :)</del> Update: this version of pytaku is
+no longer online.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/pytaku_01_chapter_progress.png b/pytaku-old/pytaku_01_chapter_progress.png
similarity index 100%
rename from content/images/pytaku_01_chapter_progress.png
rename to pytaku-old/pytaku_01_chapter_progress.png
diff --git a/content/images/pytaku_02_bookmarked_series.png b/pytaku-old/pytaku_02_bookmarked_series.png
similarity index 100%
rename from content/images/pytaku_02_bookmarked_series.png
rename to pytaku-old/pytaku_02_bookmarked_series.png
diff --git a/content/images/pytaku_03_vietnamese.png b/pytaku-old/pytaku_03_vietnamese.png
similarity index 100%
rename from content/images/pytaku_03_vietnamese.png
rename to pytaku-old/pytaku_03_vietnamese.png
diff --git a/rmit-wifi/index.dj b/rmit-wifi/index.dj
index 2382d98..f2ce3e9 100644
--- a/rmit-wifi/index.dj
+++ b/rmit-wifi/index.dj
@@ -1,10 +1,8 @@
 Title: Fix RMIT wi-fi issue in Ubuntu 13.04 and variants
-Date: 2013-06-17 08:12
-Category: tutorials
-Tags: ubuntu, linux
-Slug: fix-rmit-wifi-issue-in-ubuntu-13-04-and-variants
-Summary: The problem is NetworkManager - there's a workaround but nobody has been formally assigned to fix it.
-Thumb: images/rmit_wifi.png
+PostedAt: 2013-06-17 08:12
+Thumb: rmit_wifi.png
+
+---
 
 ## The issue
 
@@ -16,7 +14,7 @@ After days of googling, I finally pinpointed the issue: a certain version of Net
 bundled in Ubuntu 13.04 has a bug that automatically turns CA certificate usage to *true* for any
 WPA2 wifi network, even if we choose to use none in the GUI.
 
-![RMIT wi-fi settings](/images/rmit_wifi.png)
+![RMIT wi-fi settings](rmit_wifi.png)
 
 ## The solution
 
@@ -25,9 +23,10 @@ Just manually edit `/etc/NetworkManager/system-connections/RMIT-WPA`, make sure
 permission. If you're not sure how to do this, open a terminal and enter this command to open
 `gedit` with sudo permission (`mousepad` if you're using xubuntu):
 
-    :::bash
-    # Protip: DON'T use sudo for GUI programs! Use gksudo instead.
-    gksudo gedit /etc/NetworkManager/system-connections/RMIT-WPA
+```sh
+# Protip: DON'T use sudo for GUI programs! Use gksudo instead.
+gksudo gedit /etc/NetworkManager/system-connections/RMIT-WPA
+```
 
 This is a [known bug](https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1104476) and
 many have complained about it. There seems to be no developer assigned to fix it though. I'll keep
@@ -39,7 +38,8 @@ A fix has been released in GNOME upstream but not incorporated into official Ubu
 yet. An impatient contributor has created his own PPA to provide the fixed package. To install it,
 enter the following commands:
 
-    :::bash
-    sudo sudo add-apt-repository ppa:pritambaral/nms
-    sudo apt-get update
-    sudo apt-get install network-manager-gnome
+```
+sudo sudo add-apt-repository ppa:pritambaral/nms
+sudo apt-get update
+sudo apt-get install network-manager-gnome
+```
diff --git a/rmit-wifi/index.html b/rmit-wifi/index.html
new file mode 100644
index 0000000..af4cdda
--- /dev/null
+++ b/rmit-wifi/index.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Fix RMIT wi-fi issue in Ubuntu 13.04 and variants | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2013-06-17">
+        Monday, 17 Jun 2013
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Fix RMIT wi-fi issue in Ubuntu 13.04 and variants</h1>
+<section id="The-issue">
+<h2>The issue</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>
+<p>After days of googling, I finally pinpointed the issue: a certain version of NetworkManager
+bundled in Ubuntu 13.04 has a bug that automatically turns CA certificate usage to <strong>true</strong> for any
+WPA2 wifi network, even if we choose to use none in the GUI.</p>
+<p><img alt="RMIT wi-fi settings" src="rmit_wifi.png"></p>
+</section>
+<section id="The-solution">
+<h2>The solution</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
+<code>gedit</code> with sudo permission (<code>mousepad</code> if you’re using xubuntu):</p>
+<pre><code class="language-sh"># Protip: DON'T use sudo for GUI programs! Use gksudo instead.
+gksudo gedit /etc/NetworkManager/system-connections/RMIT-WPA
+</code></pre>
+<p>This is a <a href="https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1104476">known bug</a> and
+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>
+<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>
+<pre><code>sudo sudo add-apt-repository ppa:pritambaral/nms
+sudo apt-get update
+sudo apt-get install network-manager-gnome
+</code></pre>
+</section>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/rmit_wifi.png b/rmit-wifi/rmit_wifi.png
similarity index 100%
rename from content/images/rmit_wifi.png
rename to rmit-wifi/rmit_wifi.png
diff --git a/content/images/byte_databases.jpg b/sqlite-python/byte_databases.jpg
similarity index 100%
rename from content/images/byte_databases.jpg
rename to sqlite-python/byte_databases.jpg
diff --git a/sqlite-python/index.dj b/sqlite-python/index.dj
index d486774..9f1eff5 100644
--- a/sqlite-python/index.dj
+++ b/sqlite-python/index.dj
@@ -1,14 +1,14 @@
 Title: Working with SQLite in Python without an ORM or migration framework
-Date: 2022-01-30 14:11
-Summary: Some notes on handling migrations, linking the latest SQLite,
-         and sane driver defaults.
-Thumb: images/byte_databases.jpg
+PostedAt: 2022-01-30 14:11
+Thumb: byte_databases.jpg
 
-![byte-magazine-databases](/images/byte_databases.jpg)
+---
+
+![byte-magazine-databases](byte_databases.jpg)\
 _[(seriously though, BYTE covers are the best)][18]_
 
 
-I learned about SQLite's user_version pragma some time ago from a comment on
+I learned about SQLite's `user_version` pragma some time ago from a comment on
 Hacker News (as one does). Not sure which comment it was specifically, but it
 went something [like this][1]:
 
@@ -133,7 +133,7 @@ commit;
 pragma foreign_keys = on;
 ```
 
-Besides the boilerplaty dance with foreign_key pragmas and transactions, all I
+Besides the boilerplaty dance with `foreign_key` pragmas and transactions, all I
 had to do was copy the existing table definition from the aforementioned
 latest_schema.sql file, tweak it to my desired state, then do the table
 switcheroo. Again, the specific ordering of steps is important. I won't go into
diff --git a/sqlite-python/index.html b/sqlite-python/index.html
new file mode 100644
index 0000000..385fcfe
--- /dev/null
+++ b/sqlite-python/index.html
@@ -0,0 +1,209 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Working with SQLite in Python without an ORM or migration framework | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2022-01-30">
+        Sunday, 30 Jan 2022
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Working with SQLite in Python without an ORM or migration framework</h1>
+<p><img alt="byte-magazine-databases" src="byte_databases.jpg"><br>
+<em><a href="https://archive.org/details/byte-magazine">(seriously though, BYTE covers are the best)</a></em></p>
+<p>I learned about SQLite’s <code>user_version</code> pragma some time ago from a comment on
+Hacker News (as one does). Not sure which comment it was specifically, but it
+went something <a href="https://news.ycombinator.com/item?id=23510382">like this</a>:</p>
+<blockquote>
+<p>One thing you can look into using is the SQLite user_version pragma.
+We use this right now to roll our own migrators and it’s light years
+better than how migrators work for Entity Framework, et. al.</p>
+<p><a href="https://www.sqlite.org/pragma.html#pragma_user_version">https://www.sqlite.org/pragma.html#pragma_user_version</a></p>
+</blockquote>
+<p>I’d wanted to try working without an ORM for a while, and this comment gave me
+the final missing piece: a straightforward approach to SQL migrations that I
+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>
+<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>
+<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
+possible to compile a custom python version that links to a newer sqlite, but
+that would defeat the purpose of pytaku being an easy-to-deploy program. Apsw,
+on the other hand, provides a pip <a href="https://rogerbinns.github.io/apsw/download.html#i-really-want-to-use-pip">one-liner</a> that compiles and links to the
+latest sqlite. (still kinda bad, but it’s less bad than compiling custom
+python)</p>
+</section>
+<section id="It-has-the-same-defaults-as-upstream-sqlite">
+<h3>It has the same defaults as upstream sqlite</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>
+<li>
+autocommit mode is off by default
+</li>
+<li>
+<code>executescript()</code> automatically issues a <code>COMMIT</code> statement
+</li>
+</ul>
+<p>To be fair, both of them are written in the docs, and these custom defaults are
+probably to maintain consistency with <a href="https://www.python.org/dev/peps/pep-0249/">PEP 249</a>. Still, as I was learning
+sqlite, it’s frustrating to jump between sqlite docs and python docs to
+interpret the correct behavior at times. Apsw does none of those things: it’s
+simply an unopinionated, honest-to-god python binding to sqlite.</p>
+<p>To be completely honest though, in the long run it seems more reasonable to
+learn the pysqlite3 API so that I can avoid an extra dependency. I’m also now
+using Debian 11 which has a reasonably recent sqlite, so the compilation
+advantange is no longer that great.</p>
+</section>
+</section>
+<section id="A-minimum-viable-DB-migration-scheme">
+<h2>A minimum viable DB migration scheme</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>
+Finds migration files in the form of <code>./migrations/mXXXX.sql</code>
+</li>
+<li>
+Uses <code>user_version</code> pragma to figure out what migrations are pending
+</li>
+<li>
+Is forward-only—I did say that this is minimally viable didn’t I ;)
+</li>
+</ul>
+<p>Coming from Django, I missed a definitive place to see the latest definition of
+the whole db (which, in Django, is the models file). That’s why I set up the
+migrator to always write the latest db definition out to a file using
+<a href="https://github.com/nhanb/pytaku/blob/65a6c08128ebbc2b7d33a6b043798c69ac7dfebe/src/pytaku/database/migrator.py#L44-L51"><code>sqlite3 &lt;db_file&gt; .schema &gt; latest_schema.sql</code></a>, and keep that file <a href="https://github.com/nhanb/pytaku/blob/65a6c08128ebbc2b7d33a6b043798c69ac7dfebe/src/pytaku/database/migrations/latest_schema.sql">in
+version control</a>:</p>
+<pre><code class="language-sql">-- This file is auto-generated by the migration script
+-- for reference purposes only. DO NOT EDIT.
+CREATE TABLE title (
+    id text,
+    name text,
+    site text,
+    cover_ext text,
+    chapters text,
+    alt_names text,
+    descriptions text,
+    updated_at text default (datetime('now')), is_webtoon boolean not null default false, descriptions_format text not null default 'text',
+    unique(id, site)
+);
+CREATE TABLE user (
+    id integer primary key,
+    username text unique,
+    password text,
+    created_at text default (datetime('now'))
+);
+[...]
+</code></pre>
+<p>Now to address the elephant in the room: SQLite has… limited ALTER TABLE
+capabilities. The upside is it’s <a href="https://www.sqlite.org/lang_altertable.html">well-documented</a>. What this means in
+practice is that sometimes an otherwise simple <code>ALTER TABLE</code> in other RDBMS-es
+will require more manual gymnastics in SQLite: you’ll need to create a new
+table with the desired properties, copy existing data over to the new table,
+then drop the old table. There are subtle bear traps in the specific order of
+steps to take, but thankfully the docs, again, deliver: as long as you
+follow the <a href="https://www.sqlite.org/lang_altertable.html#otheralter">12 steps</a> correctly, you won’t mess up your data. It sounds
+intimidating but it’s not <em>that</em> bad. Here’s a specific example from pytaku
+where I removed a FOREIGN KEY constraint:</p>
+<pre><code class="language-sql">-- Remove foreign key from "read" table pointing to "chapter".
+-- So we can, say, mark all chapters of a title as read even if some of those
+-- chapters haven't been created.
+
+pragma foreign_keys = off; -- to let us do anything at all
+begin transaction;
+
+create table new_read (
+    user_id integer not null,
+    site text not null,
+    title_id text, -- nullable to accomodate existing mangadex rows, urgh.
+    chapter_id text not null,
+    updated_at text default (datetime('now')),
+
+    foreign key (user_id) references user (id),
+    unique(user_id, site, title_id, chapter_id)
+);
+insert into new_read select * from read;
+drop table read;
+alter table new_read rename to read;
+
+pragma foreign_key_check;
+commit;
+pragma foreign_keys = on;
+</code></pre>
+<p>Besides the boilerplaty dance with <code>foreign_key</code> pragmas and transactions, all I
+had to do was copy the existing table definition from the aforementioned
+latest_schema.sql file, tweak it to my desired state, then do the table
+switcheroo. Again, the specific ordering of steps is important. I won’t go into
+details, but I had actually tripped on a failure mode, which I then realized
+was already nicely warned against in the docs. RTFM is actually fine advice for
+projects that have good documentation, who would have thought?</p>
+</section>
+<section id="Recommended-sane-defaults">
+<h2>Recommended sane defaults</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
+what you want from a web service.</p>
+<p><a href="https://www.sqlite.org/pragma.html#pragma_foreign_keys">Enforce foreign key constraints</a>. Yep, you read that right: SQLite
+doesn’t enforce foreign key constraints by default. This is just one of the
+various consequences of SQLite being veeeeery lax about what you store. Another
+potential surprise is column types not being enforced, whose alternative only
+landed recently in the form of <a href="https://www.sqlite.org/stricttables.html">STRICT Tables</a>.</p>
+<p><a href="https://www.sqlite.org/c3ref/busy_timeout.html">Set a non-zero busytimeout</a>. Otherwise if a query is blocked, it will
+crash immediately instead of waiting for the blocking query to finish, no
+matter how short the wait is.</p>
+</section>
+<section id="A-quick-note-on-SQL-injection">
+<h2>A quick note on SQL injection</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
+parameterized queries. Python’s sqlite3 documentation also covers this, but the
+tl;dr is:</p>
+<pre><code class="language-python"># Never compose your query with string interpolation like this:
+cursor.execute(f"SELECT foo FROM bar WHERE stuff = '{user_input}';")
+
+# Use the parameter substitution API instead:
+cursor.execute('SELECT foo FROM bar WHERE stuff = ?;', (user_input,))
+</code></pre>
+<p>Congratulations! You now have better security hygiene than <a href="https://vnhacker.blogspot.com/2021/08/bkav-bi-hack-nhu-nao.html">Vietnam’s “leading”
+cybersecurity firm</a>.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/padmod/01_before.jpg b/stepmania-pad/01_before.jpg
similarity index 100%
rename from content/images/padmod/01_before.jpg
rename to stepmania-pad/01_before.jpg
diff --git a/content/images/padmod/02_sizing.jpg b/stepmania-pad/02_sizing.jpg
similarity index 100%
rename from content/images/padmod/02_sizing.jpg
rename to stepmania-pad/02_sizing.jpg
diff --git a/content/images/padmod/03_cut.jpg b/stepmania-pad/03_cut.jpg
similarity index 100%
rename from content/images/padmod/03_cut.jpg
rename to stepmania-pad/03_cut.jpg
diff --git a/content/images/padmod/05_cut.jpg b/stepmania-pad/05_cut.jpg
similarity index 100%
rename from content/images/padmod/05_cut.jpg
rename to stepmania-pad/05_cut.jpg
diff --git a/content/images/padmod/06_nonslip.jpg b/stepmania-pad/06_nonslip.jpg
similarity index 100%
rename from content/images/padmod/06_nonslip.jpg
rename to stepmania-pad/06_nonslip.jpg
diff --git a/content/images/padmod/07_nonslip.jpg b/stepmania-pad/07_nonslip.jpg
similarity index 100%
rename from content/images/padmod/07_nonslip.jpg
rename to stepmania-pad/07_nonslip.jpg
diff --git a/content/images/padmod/07a_tape.jpg b/stepmania-pad/07a_tape.jpg
similarity index 100%
rename from content/images/padmod/07a_tape.jpg
rename to stepmania-pad/07a_tape.jpg
diff --git a/content/images/padmod/08_finished.jpg b/stepmania-pad/08_finished.jpg
similarity index 100%
rename from content/images/padmod/08_finished.jpg
rename to stepmania-pad/08_finished.jpg
diff --git a/stepmania-pad/index.dj b/stepmania-pad/index.dj
index 8564c53..39d909a 100644
--- a/stepmania-pad/index.dj
+++ b/stepmania-pad/index.dj
@@ -1,14 +1,14 @@
 Title: Simplest possible stepmania soft-to-hard pad mod
-Date: 2021-02-08 12:47
-Category: side projects
-thumb: images/padmod/08_finished.jpg
+PostedAt: 2021-02-08 12:47
+Thumb: padmod/08_finished.jpg
 
+---
 
 I've been playing Stepmania on and off for years now, but only recently tried
 taping the soft dancepad to the floor. It blew my mind how much better it
 felt, since I no longer had to worry about the pad sliding or curling up:
 
-![mach 0](/images/padmod/01_before.jpg)
+![mach 0](01_before.jpg)
 
 However, taping & untaping every time I play is too much of a hurdle (and
 I want _zero_ friction for my cardio sessions). Therefore, the hard pad mod.
@@ -24,46 +24,46 @@ the bottom with a yoga mat for slip protection, then tape the softpad on top.
 Luckily, my uncle had a spare piece of plywood that somehow had the perfect
 width:
 
-![sizing](/images/padmod/02_sizing.jpg)
+![sizing](02_sizing.jpg)
 
 So my job is sawing _one single line_. How hard could it be?
 
-![cut 1](/images/padmod/03_cut.jpg)
+![cut 1](03_cut.jpg)
 
 Turns out, the saw was blunt, and my sawing technique was so bad (read:
 non-existent) so the cut keeps leaning to the right. Since I had no idea how to
 fix it, I just continued until the halfway point then flipped the whole thing
 over, hoping that at least my right-leaning sawing was consistent.
 
-![cut 2](/images/padmod/05_cut.jpg)
+![cut 2](05_cut.jpg)
 
 What do you know, it worked!
 
 Since I also had an old unused yoga mat, I checked how well it worked as a
 non-slip solution:
 
-![nonslip_1](/images/padmod/06_nonslip.jpg)
+![nonslip_1](06_nonslip.jpg)
 
 Then cut it into shape and slapped on some double tape. The board itself was
 heavy enough so just putting it on top of the mat was enough to make it stay in
 place, so the tape's only job was to keep the mat from falling off whenever I
 need to pick up & move the whole thing around.
 
-![nonslip_2](/images/padmod/07_nonslip.jpg)
+![nonslip_2](07_nonslip.jpg)
 
 Finally, taping the soft pad on the other side. I used this roll that I bought
 from the nearby [Emart](https://www.emart.com.vn/), which was hand-tearable, so
 I could work without scissors, and also easily removable, just in case I need
 to replace the soft pad in the future:
 
-![nonslip_2](/images/padmod/07a_tape.jpg)
+![nonslip_2](07a_tape.jpg)
 
 And with that, we've got the end product. It ain't pretty, but it works. Very
 well at that. A nice bonus that I didn't expect is that thanks to the mat
 underneath, the board just feels softer, therefore more comfortable, to step
 on.
 
-![tada](/images/padmod/08_finished.jpg)
+![tada](08_finished.jpg)
 
 I know serious DDR enthusiasts will [scoff
 at](https://youtu.be/sEWj2_BNG_0?t=263) the idea of using a soft pad at all,
@@ -81,12 +81,14 @@ I'd prefer it didn't happen though.
 
 P/s here it is in action:
 
+```=html
 <video controls>
   <source src="https://junk.imnhan.com/softpadmod.mp4" type="video/mp4">
   <a href="https://junk.imnhan.com/softpadmod.mp4">
     Video: https://junk.imnhan.com/softpadmod.mp4
   </a>
 </video>
+```
 
-<strike>(man I wonder if we'll ever get a Melty Blood 2)</strike>  
+{-(man I wonder if we'll ever get a Melty Blood 2)-}\
 Hot damn they're actually [doing it](https://meltyblood.typelumina.com/en/)!
diff --git a/stepmania-pad/index.html b/stepmania-pad/index.html
new file mode 100644
index 0000000..f882e33
--- /dev/null
+++ b/stepmania-pad/index.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Simplest possible stepmania soft-to-hard pad mod | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2021-02-08">
+        Monday, 08 Feb 2021
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Simplest possible stepmania soft-to-hard pad mod</h1>
+<p>I’ve been playing Stepmania on and off for years now, but only recently tried
+taping the soft dancepad to the floor. It blew my mind how much better it
+felt, since I no longer had to worry about the pad sliding or curling up:</p>
+<p><img alt="mach 0" src="01_before.jpg"></p>
+<p>However, taping &amp; untaping every time I play is too much of a hurdle (and
+I want <em>zero</em> friction for my cardio sessions). Therefore, the hard pad mod.</p>
+<p>Of course people on the internet have done this a gazillion times, such as
+<a href="https://www.angelfire.com/pro2/softpadmod/">this one</a> or <a href="https://www.youtube.com/watch?v=Soem9tnzeG0">this
+one</a>. Those guys did way more
+fiddly stuff that I personally don’t need though, such as putting thick
+vinyl under each button for better feedback, or covering the whole top with
+more vinyl. My plan is simple: cut out a piece of plywood, double-sided-tape
+the bottom with a yoga mat for slip protection, then tape the softpad on top.</p>
+<p>Luckily, my uncle had a spare piece of plywood that somehow had the perfect
+width:</p>
+<p><img alt="sizing" src="02_sizing.jpg"></p>
+<p>So my job is sawing <em>one single line</em>. How hard could it be?</p>
+<p><img alt="cut 1" src="03_cut.jpg"></p>
+<p>Turns out, the saw was blunt, and my sawing technique was so bad (read:
+non-existent) so the cut keeps leaning to the right. Since I had no idea how to
+fix it, I just continued until the halfway point then flipped the whole thing
+over, hoping that at least my right-leaning sawing was consistent.</p>
+<p><img alt="cut 2" src="05_cut.jpg"></p>
+<p>What do you know, it worked!</p>
+<p>Since I also had an old unused yoga mat, I checked how well it worked as a
+non-slip solution:</p>
+<p><img alt="nonslip_1" src="06_nonslip.jpg"></p>
+<p>Then cut it into shape and slapped on some double tape. The board itself was
+heavy enough so just putting it on top of the mat was enough to make it stay in
+place, so the tape’s only job was to keep the mat from falling off whenever I
+need to pick up &amp; move the whole thing around.</p>
+<p><img alt="nonslip_2" src="07_nonslip.jpg"></p>
+<p>Finally, taping the soft pad on the other side. I used this roll that I bought
+from the nearby <a href="https://www.emart.com.vn/">Emart</a>, which was hand-tearable, so
+I could work without scissors, and also easily removable, just in case I need
+to replace the soft pad in the future:</p>
+<p><img alt="nonslip_2" src="07a_tape.jpg"></p>
+<p>And with that, we’ve got the end product. It ain’t pretty, but it works. Very
+well at that. A nice bonus that I didn’t expect is that thanks to the mat
+underneath, the board just feels softer, therefore more comfortable, to step
+on.</p>
+<p><img alt="tada" src="08_finished.jpg"></p>
+<p>I know serious DDR enthusiasts will <a href="https://youtu.be/sEWj2_BNG_0?t=263">scoff
+at</a> the idea of using a soft pad at all,
+but really I’m just a fat dude trying to lose weight and have fun in the
+process, not trying to impress anyone in the Stamina Nation Discord or
+whatever. With that use case, I think this specific setup offers the best
+bang-for-your-buck, considering how even the lowest tiers of premade hard pads
+sell for exorbitant prices (and that’s before shipping, if they even ship to
+Vietnam at all), and DIY-ing a hard pad from scratch is way more effort.</p>
+<p>Between this and working through <a href="https://pages.cs.wisc.edu/~remzi/OSTEP/">the comet
+book</a>, I think I’ll keep myself
+entertained enough during Saigon’s potential second COVID-19 lockdown.
+I’d prefer it didn’t happen though.</p>
+<p>P/s here it is in action:</p>
+<video controls>
+  <source src="https://junk.imnhan.com/softpadmod.mp4" type="video/mp4">
+  <a href="https://junk.imnhan.com/softpadmod.mp4">
+    Video: https://junk.imnhan.com/softpadmod.mp4
+  </a>
+</video>
+<p><del>(man I wonder if we’ll ever get a Melty Blood 2)</del><br>
+Hot damn they’re actually <a href="https://meltyblood.typelumina.com/en/">doing it</a>!</p>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/tmux-italics/index.dj b/tmux-italics/index.dj
index f94f3cb..a26bea8 100644
--- a/tmux-italics/index.dj
+++ b/tmux-italics/index.dj
@@ -1,25 +1,29 @@
 Title: Enable italic text inside vim inside tmux inside gnome-terminal
-Date: 2014-08-02 16:46
-Category: tutorials
-Tags: linux, vim
-Slug: enable-italic-text-vim-tmux-gnome-terminal
-Thumb: images/italic_01_gvim.png
+PostedAt: 2014-08-02 16:46
+Thumb: italic_01_gvim.png
+---
 
-**Update**: As `egmont` pointed out in the comments: setting `TERM=xterm` inside tmux is
+*2023 Update*: I now use [kitty](https://sw.kovidgoyal.net/kitty/) terminal
+which has powerful tab & split functionalities so tmux is no longer necessary
+for my "unix IDE" use case. Removing tmux also means removing a whole class of
+compatibility issues, making it much nicer to work with terminal programs. I
+recommend giving it a try.
+
+*Update*: As `egmont` pointed out in the comments: setting `TERM=xterm` inside tmux is
 discouraged and will cause wrong behavior in some programs. Changing all instances of
 `xterm-256color` to `screen-256color` in this tutorial should work, but I'm no longer using
 gnome-terminal so I can't test that. I'm now a KDE convert by the way; italic text Just
-Works<sup>tm</sup> with Konsole. Neat, eh?
+Works^TM^ with Konsole. Neat, eh?
 
-It has bothered me for a while what I can't get terminal vim to display *italic* text. It might
+It has bothered me for a while what I can't get terminal vim to display _italic_ text. It might
 seem trivial but it makes a world of difference when I'm editing Markdown or HTML. Here's what gvim
 looks like:
 
-![](/images/italic_01_gvim.png)
+![](italic_01_gvim.png)
 
 Neat, right? This is what terminal vim shows:
 
-![](/images/italic_02_vim.png)
+![](italic_02_vim.png)
 
 I don't know about you, but the second one looks catastrophically messy and counterintuitive to me.
 Let's change that. My current setup is terminal vim running inside a tmux session on
@@ -30,11 +34,11 @@ gnome-terminal. Let's go through these things.
 Note that older versions of `gnome-terminal` do not support italic text. To check if your terminal
 does support it, run this command:
 
-```bash
+```sh
 $ echo -e "\e[3m foo \e[23m"
 ```
 
-If your version of gnome-terminal supports it, an italic *foo* will appear. If not, upgrade it! :)
+If your version of gnome-terminal supports it, an italic _foo_ will appear. If not, upgrade it! :)
 
 ## vim
 
diff --git a/tmux-italics/index.html b/tmux-italics/index.html
new file mode 100644
index 0000000..73f4e57
--- /dev/null
+++ b/tmux-italics/index.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Enable italic text inside vim inside tmux inside gnome-terminal | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2014-08-02">
+        Saturday, 02 Aug 2014
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Enable italic text inside vim inside tmux inside gnome-terminal</h1>
+<p><strong>2023 Update</strong>: I now use <a href="https://sw.kovidgoyal.net/kitty/">kitty</a> terminal
+which has powerful tab &amp; split functionalities so tmux is no longer necessary
+for my “unix IDE” use case. Removing tmux also means removing a whole class of
+compatibility issues, making it much nicer to work with terminal programs. I
+recommend giving it a try.</p>
+<p><strong>Update</strong>: As <code>egmont</code> pointed out in the comments: setting <code>TERM=xterm</code> inside tmux is
+discouraged and will cause wrong behavior in some programs. Changing all instances of
+<code>xterm-256color</code> to <code>screen-256color</code> in this tutorial should work, but I’m no longer using
+gnome-terminal so I can’t test that. I’m now a KDE convert by the way; italic text Just
+Works<sup>TM</sup> with Konsole. Neat, eh?</p>
+<p>It has bothered me for a while what I can’t get terminal vim to display <em>italic</em> text. It might
+seem trivial but it makes a world of difference when I’m editing Markdown or HTML. Here’s what gvim
+looks like:</p>
+<p><img alt="" src="italic_01_gvim.png"></p>
+<p>Neat, right? This is what terminal vim shows:</p>
+<p><img alt="" src="italic_02_vim.png"></p>
+<p>I don’t know about you, but the second one looks catastrophically messy and counterintuitive to me.
+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>
+<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"
+</code></pre>
+<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>
+<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>
+<pre><code class="language-bash"># Download a custom terminfo that defines sitm and ritm
+$ wget https://gist.githubusercontent.com/sos4nt/3187620/raw/8e13c1fec5b72d415ed2917590348451de5f8e58/xterm-256color-italic.terminfo
+# Compile it
+$ tic xterm-256color-italic.terminfo
+# Activate xterm-256color-italic.terminfo automatically
+# (edit filename accordingly if you're using another shell)
+$ echo 'export TERM=xterm-256color-italic' &gt;&gt; ~/.bashrc
+</code></pre>
+<p>Open a new terminal window and try the first command again. You should now see an italic <strong>foo</strong>. If
+not, I can’t help you any further :P</p>
+</section>
+<section id="tmux">
+<h2>tmux</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
+reset <code>$TERM</code> to <code>screen-256color</code> or something like that.</p>
+<p>If you did the previous step, the <code>export</code> command in your <code>.bashrc</code> should have overridden tmux’s
+<code>$TERM</code> value. If for some reason it doesn’t work, you can directly tell tmux to use the correct
+value. Add this line to <code>~/.tmux.conf</code>:</p>
+<pre><code>set -g default-terminal "xterm-256color-italic"
+</code></pre>
+</section>
+<section id="More-on-vim">
+<h2>More on vim</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>
+<ol>
+<li>
+<a href="http://superuser.com/questions/204743/terminal-that-supports-ansi-italic-escape-code">Terminal that supports ANSI italic escape code?</a>
+</li>
+<li>
+<a href="http://stackoverflow.com/a/21077380">gnome-terminal’s italic escape codes</a>
+</li>
+<li>
+<a href="https://alexpearce.me/2014/05/italics-in-iterm2-vim-tmux/">Enabling italic fonts in iTerm2, tmux, and vim</a>
+</li>
+</ol>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/italic_01_gvim.png b/tmux-italics/italic_01_gvim.png
similarity index 100%
rename from content/images/italic_01_gvim.png
rename to tmux-italics/italic_01_gvim.png
diff --git a/content/images/italic_02_vim.png b/tmux-italics/italic_02_vim.png
similarity index 100%
rename from content/images/italic_02_vim.png
rename to tmux-italics/italic_02_vim.png
diff --git a/ubuntu-programs/index.dj b/ubuntu-programs/index.dj
index 3e95ffb..16c7ffa 100644
--- a/ubuntu-programs/index.dj
+++ b/ubuntu-programs/index.dj
@@ -1,9 +1,7 @@
 Title: Installing programs in Ubuntu
-Date: 2013-09-06 21:03
-Category: tutorials
-Tags: ubuntu, linux
-Summary: Or the story of runtime paths, executables, packages and package managers
-Thumb: images/wget-dependencies.png
+PostedAt: 2013-09-06 21:03
+Thumb: wget-dependencies.png
+---
 
 This article will not only explain how to install stuff, but it will (hopefully) also give readers
 a better understanding about Linux's structure for storing and executing programs, ultimately
@@ -14,31 +12,34 @@ appreciate the usefulness of package managers in general.
 Let's start with something simple. Fire up your favorite text editor and create a file called
 `itc.sh` with the following content:
 
-    :::bash
-    #!/bin/bash
-
-    notify-send "Hello world!"
+```sh
+#!/bin/bash
+notify-send "Hello world!"
+```
 
 Let's say I put it at the desktop. Now open up a terminal, go to the Desktop directory and make
 our itc file executable. In case you're new, here are the commands:
 
-    :::bash
-    cd /home/your_username/Desktop
-    chmod +x itc.sh
+```sh
+cd /home/your_username/Desktop
+chmod +x itc.sh
+```
 
-**Protip**: for the first command you can also type `cd ~/Desktop`, because `~` is the shorthand for
+*Protip*: for the first command you can also type `cd ~/Desktop`, because `~` is the shorthand for
 your home directory (`/home/username`)
 
 With the `chmod` command, we made it possible to run our little script by typing its full path:
 
-    :::bash
-    ~/Desktop/itc.sh
+```sh
+~/Desktop/itc.sh
+```
 
 Another shorthand to make your life easier: `.` stands for "current directory", meaning if you are
 currently in the `~/Desktop` directory, you can run the script by simply typing:
 
-    :::bash
-    ./itc.sh
+```
+./itc.sh
+```
 
 Either way, a notification saying "Hello world!" should pop up. This is a program in its
 simplest form: an executable file. In this particular example it is a Bash script, but it's not
@@ -46,7 +47,7 @@ limited to that. It can be a Python or Ruby script, or a compiled binary file. T
 isn't even needed. You can rename it to simply `itc` and it should run just fine.
 
 > In order to run a file, you need to make it executable. This can be done with the `chmod` command
-> or via the GUI [using Nautilus's **Properties** dialog][1].
+> or via the GUI [using Nautilus's *Properties* dialog][1].
 
 ## Path
 
@@ -55,15 +56,16 @@ it, we need to specify the whole address to the file: `~/Desktop/itc` is probabl
 looking command. In order to make it possible to simply run `itc`, you need to move it to the
 `/usr/bin/` directory. This requires root permission so we'll need `sudo` too:
 
-    :::bash
-    sudo mv ~/Desktop/itc /usr/bin/itc
+```sh
+sudo mv ~/Desktop/itc /usr/bin/itc
+```
 
 We can now run our program by simply typing `itc`. You guessed it: every executable file put in
 this directory will be available as a command. There are other directories like this too. You can
 see a whole list of such directories by typing `echo $PATH` to your terminal.
 
 > To make an executable file available as a command, shove it into a directory that's included in
-> **$PATH**
+> *$PATH*
 
 ## Packages
 
@@ -71,11 +73,11 @@ Unfortunately, most programs have a lot of files instead of one, and they are sc
 different folders. Let's have a look at the files of `wget` - the downloader that's included in
 every major Linux distribution:
 
-![Wget files](/images/wget-installed-files.png)
+![Wget files](wget-installed-files.png)
 
 It's not that the developers chose to annoy us by scattering them all over the place. It's simply
-complying to Linux structure: executable files go to **/usr/bin**, man pages (user manuals that
-show up when you type `man wget`) go to **/usr/share/man**, and so on. For more complex programs,
+complying to Linux structure: executable files go to */usr/bin*, man pages (user manuals that
+show up when you type `man wget`) go to */usr/share/man*, and so on. For more complex programs,
 the number of files alone is terrifying, which makes installing and remove the program a nightmare.
 
 On another note, almost every Linux program depends on one or many other programs. This is
@@ -83,7 +85,7 @@ because of the UNIX philosophy that encourages writing each program to do one th
 well. The goal is to make each program easier to implement and maintain as well as to avoid
 duplicate work. For example, program A may provide a functionality that both programs B and C
 need. Otherwise, B and C developers both have to write code for one same functionality. In this
-case, A is called a **dependency** of B and C. However, this introduces a bunch of problems:
+case, A is called a *dependency* of B and C. However, this introduces a bunch of problems:
 
 - We need to install A before installing B
 - We should know not to install A again when we install C
@@ -95,17 +97,17 @@ This is where packages jump in. A package is basically the whole set of files of
 part of a program). It also stores necessary information such as which file goes to which
 directory, what are the dependencies of this package, etc. A special program reads the
 package, installs dependencies and puts files into their appropriate locations. This is called
-a **package manager**. Of course besides installing, a package manager also manages updates
+a *package manager*. Of course besides installing, a package manager also manages updates
 and removals of programs. Ubuntu is based on Debian, so it inherits Debian's great package manager
-called **aptitude** (or simply `apt`). Let's take a look at wget's dependencies:
+called *aptitude* (or simply `apt`). Let's take a look at wget's dependencies:
 
-![Wget files](/images/wget-dependencies.png)
+![Wget files](wget-dependencies.png)
 
 The package manager maintains a list of available packages and their dependency/dependant
 relationships. For Ubuntu, the list is updated regularly on Canonical's official servers. Everytime
 Ubuntu does the "Check for updates" thing, it is downloading the latest list of packages. And when
 Ubuntu updates, it is simply pulling newer versions of the installed packages from Canonical's
-servers too. These servers are called **repositories**. All other major Linux distributions do the
+servers too. These servers are called *repositories*. All other major Linux distributions do the
 same thing: letting the package manager and the repositories work on their thing, saving users
 time to do more interesting stuff.
 
@@ -127,7 +129,7 @@ Aptitude is only a command-line program, which is not very user-friendly. Synapt
 that provides a nice user interface that's easy to use, while internally it uses `apt` to do all
 the actual work.
 
-![Wget files](/images/wget-dependencies.png)
+![](synaptic.png)
 
 Ubuntu Software Center is more than a GUI wrapper for `apt`. It is something similar to Apple's
 appstore with all those program ratings and promotions. It's nice for beginners but the fact that
diff --git a/ubuntu-programs/index.html b/ubuntu-programs/index.html
new file mode 100644
index 0000000..f843793
--- /dev/null
+++ b/ubuntu-programs/index.html
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Installing programs in Ubuntu | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2013-09-06">
+        Friday, 06 Sep 2013
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Installing programs in Ubuntu</h1>
+<p>This article will not only explain how to install stuff, but it will (hopefully) also give readers
+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>
+<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
+notify-send "Hello world!"
+</code></pre>
+<p>Let’s say I put it at the desktop. Now open up a terminal, go to the Desktop directory and make
+our itc file executable. In case you’re new, here are the commands:</p>
+<pre><code class="language-sh">cd /home/your_username/Desktop
+chmod +x itc.sh
+</code></pre>
+<p><strong>Protip</strong>: for the first command you can also type <code>cd ~/Desktop</code>, because <code>~</code> is the shorthand for
+your home directory (<code>/home/username</code>)</p>
+<p>With the <code>chmod</code> command, we made it possible to run our little script by typing its full path:</p>
+<pre><code class="language-sh">~/Desktop/itc.sh
+</code></pre>
+<p>Another shorthand to make your life easier: <code>.</code> stands for “current directory”, meaning if you are
+currently in the <code>~/Desktop</code> directory, you can run the script by simply typing:</p>
+<pre><code>./itc.sh
+</code></pre>
+<p>Either way, a notification saying “Hello world!” should pop up. This is a program in its
+simplest form: an executable file. In this particular example it is a Bash script, but it’s not
+limited to that. It can be a Python or Ruby script, or a compiled binary file. The file extension
+isn’t even needed. You can rename it to simply <code>itc</code> and it should run just fine.</p>
+<blockquote>
+<p>In order to run a file, you need to make it executable. This can be done with the <code>chmod</code> command
+or via the GUI <a href="http://askubuntu.com/questions/35478/how-do-i-mark-a-file-as-executable-via-a-gui">using Nautilus’s <strong>Properties</strong> dialog</a>.</p>
+</blockquote>
+</section>
+<section id="Path">
+<h2>Path</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
+<code>/usr/bin/</code> directory. This requires root permission so we’ll need <code>sudo</code> too:</p>
+<pre><code class="language-sh">sudo mv ~/Desktop/itc /usr/bin/itc
+</code></pre>
+<p>We can now run our program by simply typing <code>itc</code>. You guessed it: every executable file put in
+this directory will be available as a command. There are other directories like this too. You can
+see a whole list of such directories by typing <code>echo $PATH</code> to your terminal.</p>
+<blockquote>
+<p>To make an executable file available as a command, shove it into a directory that’s included in
+<strong>$PATH</strong></p>
+</blockquote>
+</section>
+<section id="Packages">
+<h2>Packages</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>
+<p><img alt="Wget files" src="wget-installed-files.png"></p>
+<p>It’s not that the developers chose to annoy us by scattering them all over the place. It’s simply
+complying to Linux structure: executable files go to <strong>/usr/bin</strong>, man pages (user manuals that
+show up when you type <code>man wget</code>) go to <strong>/usr/share/man</strong>, and so on. For more complex programs,
+the number of files alone is terrifying, which makes installing and remove the program a nightmare.</p>
+<p>On another note, almost every Linux program depends on one or many other programs. This is
+because of the UNIX philosophy that encourages writing each program to do one thing, and do it
+well. The goal is to make each program easier to implement and maintain as well as to avoid
+duplicate work. For example, program A may provide a functionality that both programs B and C
+need. Otherwise, B and C developers both have to write code for one same functionality. In this
+case, A is called a <strong>dependency</strong> of B and C. However, this introduces a bunch of problems:</p>
+<ul>
+<li>
+We need to install A before installing B
+</li>
+<li>
+We should know not to install A again when we install C
+</li>
+<li>
+We must be careful not to remove A if we are still using B or C
+</li>
+<li>
+What if B and C require different versions of A?
+</li>
+<li>
+I can go on…
+</li>
+</ul>
+<p>This is where packages jump in. A package is basically the whole set of files of a program (or a
+part of a program). It also stores necessary information such as which file goes to which
+directory, what are the dependencies of this package, etc. A special program reads the
+package, installs dependencies and puts files into their appropriate locations. This is called
+a <strong>package manager</strong>. Of course besides installing, a package manager also manages updates
+and removals of programs. Ubuntu is based on Debian, so it inherits Debian’s great package manager
+called <strong>aptitude</strong> (or simply <code>apt</code>). Let’s take a look at wget’s dependencies:</p>
+<p><img alt="Wget files" src="wget-dependencies.png"></p>
+<p>The package manager maintains a list of available packages and their dependency/dependant
+relationships. For Ubuntu, the list is updated regularly on Canonical’s official servers. Everytime
+Ubuntu does the “Check for updates” thing, it is downloading the latest list of packages. And when
+Ubuntu updates, it is simply pulling newer versions of the installed packages from Canonical’s
+servers too. These servers are called <strong>repositories</strong>. All other major Linux distributions do the
+same thing: letting the package manager and the repositories work on their thing, saving users
+time to do more interesting stuff.</p>
+<p>Here are some basic commands to get you started:</p>
+<ul>
+<li>
+<code>sudo apt-get install package-name</code> to install package
+</li>
+<li>
+<code>sudo apt-get remove package-name</code> - it’s obvious isn’t it?
+</li>
+<li>
+<code>sudo apt-get update</code> - update package list. Note that it only updates the list, not the packages
+</li>
+<li>
+<code>sudo apt-get upgrade</code> - upgrade packages to their latest versions
+</li>
+</ul>
+<p>There will be programs that are not available on the official repositories, but are provided as
+package files (Dropbox for example). Remember that the right package format for Ubuntu is <code>.deb</code>
+files. Do not open <code>.rpm</code> files since they are for Fedora’s package manager called <code>yum</code>. When
+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>
+<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>
+<p><img alt="" src="synaptic.png"></p>
+<p>Ubuntu Software Center is more than a GUI wrapper for <code>apt</code>. It is something similar to Apple’s
+appstore with all those program ratings and promotions. It’s nice for beginners but the fact that
+it hides the details like dependency list makes it undesirable for intermediate users. If you are
+comfortable with Synaptic, I strongly recommend using it as your main way to install/uninstall
+stuff. But ultimately, using the command-line <code>apt</code> always is the fastest way.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/synaptic.png b/ubuntu-programs/synaptic.png
similarity index 100%
rename from content/images/synaptic.png
rename to ubuntu-programs/synaptic.png
diff --git a/content/images/wget-dependencies.png b/ubuntu-programs/wget-dependencies.png
similarity index 100%
rename from content/images/wget-dependencies.png
rename to ubuntu-programs/wget-dependencies.png
diff --git a/content/images/wget-installed-files.png b/ubuntu-programs/wget-installed-files.png
similarity index 100%
rename from content/images/wget-installed-files.png
rename to ubuntu-programs/wget-installed-files.png
diff --git a/video-streaming-1/index.dj b/video-streaming-1/index.dj
index e879727..dd4f9e2 100644
--- a/video-streaming-1/index.dj
+++ b/video-streaming-1/index.dj
@@ -1,19 +1,19 @@
 Title: Towards an acceptable video playing experience
-Date: 2020-04-26 10:06
-Category: side projects
+PostedAt: 2020-04-26 10:06
+---
 
 I watch movies and TV shows.
 Naturally, I have some strong preferences on how to view them:
 
-**English subtitles**. Most things I watch are in English.
+*English subtitles*. Most things I watch are in English.
 Although I'm perfectly comfortable with face-to-face English conversations, I
 just can't keep up with English dialogue in movies.
 I also don't want to put up with badly translated subs, so English subtitles
 they must be. This rules out most Vietnamese "netflixes".
 
-**1080p**, unless it's ancient or super rare stuff.
+*1080p*, unless it's ancient or super rare stuff.
 
-**Streamable from tablets**. I shouldn't need to turn on my PC just to catch up
+*Streamable from tablets*. I shouldn't need to turn on my PC just to catch up
 on the latest Better Call Saul episode.
 
 In 2020, there sure are a variety of options available, all of which fall short
@@ -98,12 +98,14 @@ authentication behind the scene, exposing a plain HTTP streaming endpoint so
 
 Here's what it looks like in action:
 
+```=html
 <video controls>
   <source src="https://junk.imnhan.com/gflick.mp4" type="video/mp4">
   <a href="https://junk.imnhan.com/gflick.mp4">
     Video: https://junk.imnhan.com/gflick.mp4
   </a>
 </video>
+```
 
 It can run just fine as a local server, but cumbersome and not practical on
 tablets, so I put it on a publicly accessible server, protected by nginx which
@@ -140,4 +142,4 @@ The sequel is out: [Streaming videos from Google Drive - a second attempt][8]
 [5]: https://github.com/nhanb/gflick
 [6]: https://github.com/nhanb/mpv-gdrive
 [7]: https://github.com/nhanb/drivein
-[8]: /posts/streaming-videos-from-google-drive-a-second-attempt/
+[8]: ../video-streaming-2/
diff --git a/video-streaming-1/index.html b/video-streaming-1/index.html
new file mode 100644
index 0000000..f4e78a8
--- /dev/null
+++ b/video-streaming-1/index.html
@@ -0,0 +1,174 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Towards an acceptable video playing experience | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2020-04-26">
+        Sunday, 26 Apr 2020
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Towards an acceptable video playing experience</h1>
+<p>I watch movies and TV shows.
+Naturally, I have some strong preferences on how to view them:</p>
+<p><strong>English subtitles</strong>. Most things I watch are in English.
+Although I’m perfectly comfortable with face-to-face English conversations, I
+just can’t keep up with English dialogue in movies.
+I also don’t want to put up with badly translated subs, so English subtitles
+they must be. This rules out most Vietnamese “netflixes”.</p>
+<p><strong>1080p</strong>, unless it’s ancient or super rare stuff.</p>
+<p><strong>Streamable from tablets</strong>. I shouldn’t need to turn on my PC just to catch up
+on the latest Better Call Saul episode.</p>
+<p>In 2020, there sure are a variety of options available, all of which fall short
+in some ways:</p>
+<ul>
+<li>
+<p>Shady ad-infested Vietnamese movie streaming sites (phimmoi etc): Obnoxious
+pop-up tabs aside, they always <a href="https://kipalog.com/posts/Cac-web-phim-da-giam-99-99--chi-phi-bang-google-drive-nhu-the-nao">abuse Google Drive</a> (or even Facebook?)
+storage behind the scene. Problem is Google Drive encoding is lossy as hell,
+so even at 1080p they look noticeably worse than the original. Also they
+almost always come with hardcoded Vietnamese subs.</p>
+</li>
+<li>
+<p>Netflix clones by big ISPs: Pathetic catalogues. Vietnamese subs.</p>
+</li>
+<li>
+<p>Netflix itself: Actually quite good thanks to usable Android app, but besides
+the increasingly shitty catalogue, it’s <a href="https://help.netflix.com/en/node/23742">impossible to get 1080p from
+Linux</a>. Also I hate that I can’t manually set the video quality: even if
+my current connection gets slow I’d rather pause and wait for buffering
+instead of putting up with a pixelated 480p mess. I still have my Netflix
+subscription today, but only grudgingly.</p>
+</li>
+<li>
+<p>“dude, like, just torrent it”. Solid advice since torrents usually come with
+embedded English sub, but it requires actually downloading the thing first,
+and can’t easily switch devices without moving the file along.</p>
+</li>
+<li>
+<p>Setting up a torrent + plex server? That would require (1) ample disk space,
+(2) generous network bandwidth, (3) actual horsepower for transcoding and (4)
+fast enough network access from home or wherever I watch movies from.</p>
+<ul>
+<li>
+<p>A local NAS-style server satisfies (1), (3) &amp; (4) but struggles with (2),
+and I don’t want it to hog my home internet pipes.</p>
+</li>
+<li>
+<p>Finding a VPS service with (1) &amp; (2) is doable, but (3) gets expensive
+fast and usually they’re in the US or EU which can never have (4). I’m
+actually running a seedbox on Ramnode but can’t run plex on it because of
+lack of (3) and (4). If I’m willing to pay more I can get a Hetzner
+dedicated server which can probably do (3) but (4) gets even worse.</p>
+</li>
+</ul>
+</li>
+</ul>
+<section id="Remote-seedbox-Google-Drive">
+<h2>Remote seedbox + Google Drive</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>
+<p>Since Transmission supports hooks via external scripts, I set it up so that
+downloaded torrents get uploaded to my Google Drive using <code>rclone</code>.</p>
+<p>Now whenever I find something interesting that’s not on Netflix, I look for a
+working torrent file and tell my seedbox to get it. Thanks to the web interface
+I can do it from both my PC and tablet. I don’t have to keep my devices running
+so it doesn’t matter if the torrent is not well-seeded and takes a long time.</p>
+<p>Once the file lands on Google Drive, I can either:</p>
+<ul>
+<li>
+<p>watch it directly from GDrive’s web/Android app if I don’t care about
+subtitles or original quality, or</p>
+</li>
+<li>
+<p>download the file first and watch properly</p>
+</li>
+</ul>
+<p>The latter is not ideal.</p>
+</section>
+<section id="Enter-gflick">
+<h2>Enter gflick</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
+the file telling where each track lies within the file. The player can download
+just the metadata first, then the subtitle track, then the actual video track
+starting from a specific position. This is only possible if the http server
+supports partial content download <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range">via the <code>Range</code> header</a>.</p>
+<p>Google Drive does have a “direct link” API in the form of
+<code>https://www.googleapis.com/drive/v3/files/&lt;fileId&gt;?alt=media</code>, which luckily
+supports partial download. The bad news is downloading private files requires
+authentication via a bearer token. The only HTTP authentication scheme that
+these players support, as far as I know, is <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme">Basic auth</a>.</p>
+<p>So I wrote <a href="https://github.com/nhanb/gflick">gflick</a>, which is practically an HTTP proxy that does Google
+authentication behind the scene, exposing a plain HTTP streaming endpoint so
+<code>mpv</code> and the like can use without modification.</p>
+<p>Here’s what it looks like in action:</p>
+<video controls>
+  <source src="https://junk.imnhan.com/gflick.mp4" type="video/mp4">
+  <a href="https://junk.imnhan.com/gflick.mp4">
+    Video: https://junk.imnhan.com/gflick.mp4
+  </a>
+</video>
+<p>It can run just fine as a local server, but cumbersome and not practical on
+tablets, so I put it on a publicly accessible server, protected by nginx which
+does TLS and Basic Auth. As mentioned earlier, good video players can do basic
+auth out of the box. Gflick also exposes a simple web interface to browse my
+Google Drives, so now I can browse my drive on any pc/tablet, and watch things
+with full seek, subtitle/audio track support, right?</p>
+<p>Not quite. While desktop versions of these players work fine, their Android
+versions won’t play it. Now I regret selling my Surface Go! :(</p>
+<p>And… that’s where I’m stuck at the moment. Not sure if I should buy one of
+those Chinese Surface knock-offs or what.</p>
+</section>
+<section id="Other-failed-attempts">
+<h2>Other failed attempts</h2>
+<ul>
+<li>
+<a href="https://github.com/nhanb/mpv-gdrive">mpv-gdrive</a>: Using mpv’s lua scripting API to automatically set the
+correct bearer auth headers. Worked fine on desktop, failed miserably on
+android.
+</li>
+<li>
+<a href="https://github.com/nhanb/drivein">drivein</a>: Uses <code>rclone mount</code>. Worked fine on desktop, android wouldn’t
+allow mounting without root.
+</li>
+</ul>
+<p>Also both of those required setting up each client device. Not ideal.</p>
+<section id="Update-June-10-2020">
+<h3>Update June 10, 2020</h3>
+<p>The sequel is out: <a href="../video-streaming-2/">Streaming videos from Google Drive - a second attempt</a></p>
+</section>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/content/images/gflick_01_mobile.png b/video-streaming-2/gflick_01_mobile.png
similarity index 100%
rename from content/images/gflick_01_mobile.png
rename to video-streaming-2/gflick_01_mobile.png
diff --git a/video-streaming-2/index.dj b/video-streaming-2/index.dj
index f96b495..121ecc9 100644
--- a/video-streaming-2/index.dj
+++ b/video-streaming-2/index.dj
@@ -1,17 +1,19 @@
 Title: Streaming videos from Google Drive: a second attempt
-Date: 2020-06-10 08:25
-Category: side projects
+PostedAt: 2020-06-10 08:25
+---
 
-**TL;DR:** I improved the Google Drive video streaming experience mentioned in
+*TL;DR:* I improved the Google Drive video streaming experience mentioned in
 an [earlier blog post][1]. It now works like this on an Android phone with
 mpv-android installed:
 
+```=html
 <video controls>
   <source src="https://junk.imnhan.com/gflick-phone-demo.mp4" type="video/mp4">
   <a href="https://junk.imnhan.com/gflick-phone-demo.mp4">
     Video: https://junk.imnhan.com/gflick-phone-demo.mp4
   </a>
 </video>
+```
 
 The longer version follows.
 
@@ -28,10 +30,12 @@ advertised to support older TLSes (in either `Intermediate` or `Old` mode),
 didn't actually work in practice. [SSL tests][3] always reported that the only
 working SSL/TLS protocol was TLSv1.3.
 
-> The issue was pinpointed thanks to reading the android device's logcat
-> output. Did you know that in order to read logcat you only need to install
-> some [8-megabyte package][7] instead of the whole android studio behemoth? I
-> do now!
+::: sidenote
+I pinpointed this issue from reading the android device's logcat output.
+Did you know that in order to read logcat you only need to install
+some [8MiB package][7] instead of the whole Android Studio behemoth? I
+do now!
+:::
 
 As luck would have it, [Caddy v2][4] was recently released and they even
 provided a Debian repo! I had used Caddy v1 in the past but my impression was
@@ -41,13 +45,15 @@ still extra busywork. This combined with the hassle of having to compile my own
 binary bounced me back to nginx. Both of these issues have been addressed in
 v2, so there's really no reason to keep wrestling with nginx + certbot anymore.
 
-> On that note, to this day I still haven't figured out how to make the
-> nginx/certbot combo play nice with ansible. Problem is, certbot's nginx
-> plugin wants to mutate the nginx config file itself, so the nginx configs
-> before vs after certbot runs are decidedly different. This requires
-> ridiculous gymnastics to mold into an ansible play - and don't even get me
-> started on multi-site setups. A [Caddyfile][6], on the other hand, simply
-> gets out of your way.
+::: sidenote
+On that note, to this day I still haven't figured out how to make the
+nginx/certbot combo play nice with ansible. Problem is, certbot's nginx
+plugin wants to mutate the nginx config file itself, so the nginx configs
+before vs after certbot runs are decidedly different. This requires
+ridiculous gymnastics to mold into an ansible play - and don't even get me
+started on multi-site setups. A [Caddyfile][6], on the other hand, simply
+gets out of your way.
+:::
 
 Now that TLS is settled, I also made some changes to the usage flow:
 
@@ -56,7 +62,7 @@ Now that TLS is settled, I also made some changes to the usage flow:
 The authentication responsibility has been moved from nginx/caddy to the python
 application itself to enable more fine-grained control:
 
-Every route, except for the video-serving `/v/*`, requires a user_token cookie.
+Every route, except for the video-serving `/v/*`, requires a `user_token` cookie.
 If it doesn't exist, redirect to `/login`, which will let user submit a
 password in order to get the user token back. User token is a 128-byte string
 that's regenerated every time the python script restarts. I should probably
@@ -74,18 +80,18 @@ be done for expiration mechanisms but the foundations are there.
 
 ### Aesthetics
 
-The web interface has been revamped to make it easier for <strike>fat-fingered
-people on $current_year's trendy stupidly thin</strike> phones. Also present
+The web interface has been revamped to make it easier for {-fat-fingered
+people on $current_year's trendy stupidly thin-} phones. Also present
 are folder icons and thumbnails, so it finally gives me everything I want from
 Google Drive's web UI and nothing that I don't. Fun fact: it works on
 [NetSurf][8] too (but then again why wouldn't it?).
 
-![gflick screenshot](/images/gflick_01_mobile.png)
+![gflick screenshot](gflick_01_mobile.png)
 
 
 ## What's the catch?
 
-**Client device is solely responsible for decoding the raw file.** This is both
+*Client device is solely responsible for decoding the raw file.* 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
 software decoding and some devices are just too weak to do so smoothly. My
@@ -94,7 +100,7 @@ Curiously, my crappy Mi A3 phone yields better performance, although stutters
 still happen here and there. More modest h264 movies play flawlessly, for what
 it's worth.
 
-**This is a proof of concept and the codebase quality reflects that.** I'm in
+*This is a proof of concept and the codebase quality reflects that.* I'm in
 the middle of cleaning it up for pypi friendliness and xdg compliance, but
 currently stuck when porting from std's http server to bottlepy. The current
 dirty codebase is working fine for me so I'm in no hurry though...
diff --git a/video-streaming-2/index.html b/video-streaming-2/index.html
new file mode 100644
index 0000000..dad7e5d
--- /dev/null
+++ b/video-streaming-2/index.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Streaming videos from Google Drive: a second attempt | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2020-06-10">
+        Wednesday, 10 Jun 2020
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Streaming videos from Google Drive: a second attempt</h1>
+<p><strong>TL;DR:</strong> I improved the Google Drive video streaming experience mentioned in
+an <a href="/posts/towards-an-acceptable-video-playing-experience/">earlier blog post</a>. It now works like this on an Android phone with
+mpv-android installed:</p>
+<video controls>
+  <source src="https://junk.imnhan.com/gflick-phone-demo.mp4" type="video/mp4">
+  <a href="https://junk.imnhan.com/gflick-phone-demo.mp4">
+    Video: https://junk.imnhan.com/gflick-phone-demo.mp4
+  </a>
+</video>
+<p>The longer version follows.</p>
+<p><a href="/posts/towards-an-acceptable-video-playing-experience/">Previously</a> I was writing a proxy of sorts that adapted Google Drive’s
+“bearer token” auth to the more widely supported “basic auth” so I could watch
+movies. I was stuck at the point where desktop video players could stream just
+fine while their android ports would do nothing.</p>
+<p>Turns out it was a TLS issue: I configured nginx to use TLSv1.3 which is the
+latest and greatest, but mpv/vlc on android came bundled with older TLS
+libraries which only supported up to v1.2. This led me to another surprise: the
+nginx config generated by <a href="https://ssl-config.mozilla.org/#server=nginx&amp;version=1.17.7&amp;config=intermediate&amp;openssl=1.1.1d&amp;guideline=5.4">Mozilla’s SSL Configuration Generator</a>, while
+advertised to support older TLSes (in either <code>Intermediate</code> or <code>Old</code> mode),
+didn’t actually work in practice. <a href="https://www.ssllabs.com/ssltest/">SSL tests</a> always reported that the only
+working SSL/TLS protocol was TLSv1.3.</p>
+<div class="sidenote">
+<p>I pinpointed this issue from reading the android device’s logcat output.
+Did you know that in order to read logcat you only need to install
+some <a href="https://pkgs.org/search/?q=android-tools">8MiB package</a> instead of the whole Android Studio behemoth? I
+do now!</p>
+</div>
+<p>As luck would have it, <a href="https://caddyserver.com/v2">Caddy v2</a> was recently released and they even
+provided a Debian repo! I had used Caddy v1 in the past but my impression was
+that despite their pitch of a “download and run” experience, actual <a href="https://github.com/caddyserver/caddy/tree/v1.0.4/dist/init/linux-systemd">extra
+work</a> was required - it was straightforward and well-documented, but it was
+still extra busywork. This combined with the hassle of having to compile my own
+binary bounced me back to nginx. Both of these issues have been addressed in
+v2, so there’s really no reason to keep wrestling with nginx + certbot anymore.</p>
+<div class="sidenote">
+<p>On that note, to this day I still haven’t figured out how to make the
+nginx/certbot combo play nice with ansible. Problem is, certbot’s nginx
+plugin wants to mutate the nginx config file itself, so the nginx configs
+before vs after certbot runs are decidedly different. This requires
+ridiculous gymnastics to mold into an ansible play - and don’t even get me
+started on multi-site setups. A <a href="https://github.com/nhanb/gflick/blob/4dd3dbdbdfe8de66337ed0a2fe420dd0e1d72f39/caddy/gflick">Caddyfile</a>, on the other hand, simply
+gets out of your way.</p>
+</div>
+<p>Now that TLS is settled, I also made some changes to the usage flow:</p>
+<section id="Authentication">
+<h3>Authentication</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.
+If it doesn’t exist, redirect to <code>/login</code>, which will let user submit a
+password in order to get the user token back. User token is a 128-byte string
+that’s regenerated every time the python script restarts. I should probably
+write a janitor script to periodically regenerate it instead.</p>
+<p>When user navigates to a video, a unique 128-byte slug is generated just for it
+and the video can now be directly streamed at <code>/v/&lt;slug&gt;</code>, with no
+authentication required. Currently slugs older than 1 day are wiped on python
+application startup, but then, like user token, I should probably stop relying
+on the script restarting to do cleanup operations.</p>
+<p>With this setup I can freely share the <code>/v/&lt;slug&gt;</code> url to other people without
+leaking any auth credentials, and they eventually expire too. There’s tuning to
+be done for expiration mechanisms but the foundations are there.</p>
+</section>
+<section id="Aesthetics">
+<h3>Aesthetics</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
+Google Drive’s web UI and nothing that I don’t. Fun fact: it works on
+<a href="https://www.netsurf-browser.org/">NetSurf</a> too (but then again why wouldn’t it?).</p>
+<p><img alt="gflick screenshot" src="gflick_01_mobile.png"></p>
+</section>
+<section id="What-s-the-catch">
+<h2>What’s the catch?</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
+software decoding and some devices are just too weak to do so smoothly. My
+Amazon Fire HD 10 tablet suffers greatly when playing 1080p 10bit anime.
+Curiously, my crappy Mi A3 phone yields better performance, although stutters
+still happen here and there. More modest h264 movies play flawlessly, for what
+it’s worth.</p>
+<p><strong>This is a proof of concept and the codebase quality reflects that.</strong> I’m in
+the middle of cleaning it up for pypi friendliness and xdg compliance, but
+currently stuck when porting from std’s http server to bottlepy. The current
+dirty codebase is working fine for me so I’m in no hurry though…</p>
+</section>
+<section id="In-conclusion">
+<h2>In conclusion</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
+video player, then everything works out of the box. This is <em>almost</em> as
+convenient as Netflix, but without the stupid quality restriction on
+non-sanctioned devices. I probably need to upgrade to a beefier tablet though.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/video-streaming-3/index.html b/video-streaming-3/index.html
index 471725e..944f2aa 100644
--- a/video-streaming-3/index.html
+++ b/video-streaming-3/index.html
@@ -58,7 +58,7 @@ <h1>The video streaming finale, or why put.io is awesome</h1>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>
 
diff --git a/vim-open-link/index.dj b/vim-open-link/index.dj
index 9a85fdb..62e92c3 100644
--- a/vim-open-link/index.dj
+++ b/vim-open-link/index.dj
@@ -1,5 +1,6 @@
 Title: Opening http link under the cursor in vim
-Date: 2021-08-07 11:37
+PostedAt: 2021-08-07 11:37
+---
 
 Mr. [Walter Bright](https://www.walterbright.com/), creator of the D
 programming language, recently
@@ -94,7 +95,7 @@ Some interesting points:
 
 - `ID2/SRE/CSI` are the prefixes that I know of. No idea if there are any
   other. Would be trivial to add later anyway.
-- Because the pattern of l:jira_id is very simple, I don't even need to
+- Because the pattern of `l:jira_id` is very simple, I don't even need to
   shellescape() this one.
 - I didn't even bother to refactor common stuff between the OpenURL() and
   OpenJira().
diff --git a/vim-open-link/index.html b/vim-open-link/index.html
new file mode 100644
index 0000000..d30d229
--- /dev/null
+++ b/vim-open-link/index.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Opening http link under the cursor in vim | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2021-08-07">
+        Saturday, 07 Aug 2021
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Opening http link under the cursor in vim</h1>
+<p>Mr. <a href="https://www.walterbright.com/">Walter Bright</a>, creator of the D
+programming language, recently
+<a href="https://news.ycombinator.com/item?id=28090272">commented</a> on Hacker News:</p>
+<blockquote>
+<p>And for bug fixes, reference the issue which often gives a detailed
+explanation for why the code is a certain way:</p>
+<p><a href="https://github.com/dlang/dmd/blob/master/src/dmd/backend/cgxmm.d#L1210">https://github.com/dlang/dmd/blob/master/src/dmd/backend/cgxmm.d#L1210</a></p>
+<p>Ever since I enhanced the editor I use to open the browser on links, this
+sort of thing has proven to be very, very handy.</p>
+</blockquote>
+<p>I’ve never had any issue with opening links from vim: I have <code>&lt;leader&gt;y</code> set up
+in Visual mode to yank stuff into the system-wide clipboard which I can then
+paste into the browser. However, ever since I mapped <code>&lt;leader&gt;gh</code> to trigger
+<a href="https://github.com/tpope/vim-fugitive/blob/2dc08dfe354ed5400f5cdb3d5009dcd4024aac8a/doc/fugitive.txt#L213"><code>:GBrowse</code></a> that opens a browser tab instantly, the old “select, copy,
+alt-tab to browser, ctrl+t, ctrl+v” flow started to feel… prehistoric. Mr.
+Bright’s comment gave me the final nudge to actually go ahead and set it up.</p>
+<p>The good folks from the <a href="https://stackoverflow.com/questions/9458294/open-url-under-cursor-in-vim-with-browser">developer encyclopedia</a> suggested <code>gx</code> but for some
+reason, setting <code>g:netrw_browsex_viewer</code> <a href="https://github.com/vim/vim/issues/4738">didn’t seem to do anything</a> so the
+command would always <code>wget</code> the link then tell the browser to open that
+downloaded file. Therefore, I cobbled together this snippet which was adapted
+from those stackoverflow &amp; github threads:</p>
+<pre><code class="language-vimscript">function! OpenURL()
+  let l:url = matchstr(expand("&lt;cWORD&gt;"), 'https\=:\/\/[^ &gt;,;()]*')
+  if l:url != ""
+    let l:url = shellescape(l:url, 1)
+    let l:command = "!xdg-open ".l:url
+    echo l:command
+    silent exec l:command
+  else
+    echo "No URL found under cursor."
+  endif
+endfunction
+
+nnoremap gl :call OpenURL()&lt;cr&gt;
+</code></pre>
+<p><em>(if you’re on a Mac, replacing <code>xdg-open</code> with <code>open</code> will probably
+do the same thing)</em></p>
+<p>Now whenever I have my cursor on an http(s) url, I can type <code>gl</code> from normal
+mode and xdg-open will use my default browser to open it up. This could be
+extended to any other scheme like <code>mailto</code> or <code>ftp</code> but I don’t have any
+practical use for them right now so that will do.</p>
+<p>One drawback is if there’s a whitespace in the URL (which is bad practice
+anyway), my regex won’t match the whole thing. In such cases I’d rather resort
+to good old manual visual mode than try to be clever and make my URL detecting
+logic exponentially more complex. I’d take simple software with obvious, easily
+understood behavior over overcomplicated, (possibly) subtly broken balls of mud
+any day.</p>
+<p>By the way, if you looked at my script and got spooked by the idea of executing
+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>
+<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>
+<pre><code>[SRE-123456] Finally fix the goddamn pipeline
+</code></pre>
+<p>That’s no URL, but the jira ticket ID pattern is pretty simple, so I simply
+altered the regexp a bit like this:</p>
+<pre><code class="language-vimscript">function! OpenJira()
+  let l:jira_id = toupper(matchstr(expand("&lt;cWORD&gt;"), '\c\(id2\|sre\|csi\)-[0-9]\+'))
+  if l:jira_id != ""
+    let l:command = "!xdg-open https://my-company.atlassian.net/browse/".l:jira_id
+    echo l:command
+    silent exec l:command
+  else
+    echo "No Jira ticket found under cursor."
+  endif
+endfunction
+nnoremap gj :call OpenJira()&lt;cr&gt;
+</code></pre>
+<p>Voila! Now I can press <code>gj</code> to open any atlassian ticket from just its ID.</p>
+<p>Some interesting points:</p>
+<ul>
+<li>
+<code>ID2/SRE/CSI</code> are the prefixes that I know of. No idea if there are any
+other. Would be trivial to add later anyway.
+</li>
+<li>
+Because the pattern of <code>l:jira_id</code> is very simple, I don’t even need to
+shellescape() this one.
+</li>
+<li>
+I didn’t even bother to refactor common stuff between the OpenURL() and
+OpenJira().
+</li>
+</ul>
+<p>On a more big-picture note, I can afford to make seemingly sloppy decisions
+precisely because this serves only myself, and my specific use cases are
+usually narrow. It’s not very general, but it works, and works precisely the
+way I want it. This is one of the reasons I’ve always preferred simple tooling
+that I can build upon, rather than following the prescribed workflows of more
+full-fledged IDEs.</p>
+<p>I’m not bashing IDEs, and I’m in no way promoting vim or <a href="https://github.com/DigitalMars/med">rolling your own
+emacs</a>. I’m firmly in the “use whatever
+you’re comfortable with” camp. I think the whole idea of editor/IDE wars is
+juvenile, dumb and counterproductive (all software sucks in some way anyway,
+fight me). Showing nifty tricks you can do with your tools, inspiring others to
+either check them out or implement those on their own tools, just like how Mr.
+Bright has done with his little comment, is a much better use of everyone’s
+time. I think.</p>
+</section>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/virtualenvwrapper/index.dj b/virtualenvwrapper/index.dj
index abf774d..03ca6a3 100644
--- a/virtualenvwrapper/index.dj
+++ b/virtualenvwrapper/index.dj
@@ -1,10 +1,6 @@
 Title: Virtualenv(wrapper), python2 and python3
-Date: 2014-12-16 21:35
-Category: tutorials
-Tags: linux, vim, python
-Slug: virtualenwrapper-python2-python3
-Summary: TL;DR: Install virtualenv via `apt-get`, not `pip`, then `mkvirtualenv -p /path/to/python/executable`.
-
+PostedAt: 2014-12-16 21:35
+---
 
 Virtualenv and virtualenvwrapper make it super easy to have a sandboxed python environment for each
 of your projects, no doubt about it (if you're not using them already, feel free to google how to
diff --git a/virtualenvwrapper/index.html b/virtualenvwrapper/index.html
new file mode 100644
index 0000000..36a4241
--- /dev/null
+++ b/virtualenvwrapper/index.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8" />
+  <title>Virtualenv(wrapper), python2 and python3 | Hi, I&#39;m Nhân</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.xml">
+  <link rel="stylesheet" href="/_theme/base.css">
+</head>
+
+<body>
+
+<link rel="stylesheet" href="/_theme/navbar.css">
+<nav>
+  <a href="/">Home</a>
+  <a href="/about/">About</a>
+  <a href="/projects/">Projects</a>
+  <span class="posted-on">
+    Posted on
+    <time datetime="2014-12-16">
+        Tuesday, 16 Dec 2014
+    </time>
+  </span>
+
+</nav>
+<hr class="nav-hr">
+
+
+<main>
+<h1>Virtualenv(wrapper), python2 and python3</h1>
+<p>Virtualenv and virtualenvwrapper make it super easy to have a sandboxed python environment for each
+of your projects, no doubt about it (if you’re not using them already, feel free to google how to
+get started).</p>
+<p>By default, <code>mkvirtualenv my-env-name</code> will create a virtualenv using the OS’s default python
+version (in Ubuntu’s case, that’s python2). If you want a virtualenv that has <code>python</code> mapped to
+python3 instead, use the <code>-p</code> argument:</p>
+<pre><code class="language-bash">$ mkvirtualenv -p `which python3` my-env-name
+# assumming you have python3 installed already, of course!
+</code></pre>
+<p>However, on Ubuntu this will fail if you installed virtualenv as a pip package. If that’s the case,
+simply remove it and install the Ubuntu package instead. It goes like this for Ubuntu 14.04:</p>
+<pre><code class="language-bash">$ sudo pip uninstall virtualenv
+$ sudo apt-get install python-virtualenv
+$ sudo pip install virtualenvwrapper  # yes, you can install virtualenvwrapper via pip
+$ mkvirtualenv -p `which python3` my-env-name
+</code></pre>
+<p>Neat, eh?</p>
+
+</main>
+
+<footer>
+© 2013–2023 nhanb<br>
+Made with <a href="https://github.com/nhanb/s4g">s4g</a>
+</footer>
+
+</body>
+
+</html>
diff --git a/yaks/index.html b/yaks/index.html
index 808cd40..2919757 100644
--- a/yaks/index.html
+++ b/yaks/index.html
@@ -92,7 +92,7 @@ <h2>Home server</h2>
 </main>
 
 <footer>
-© 2014–2023 nhanb<br>
+© 2013–2023 nhanb<br>
 Made with <a href="https://github.com/nhanb/s4g">s4g</a>
 </footer>