about summary refs log tree commit diff homepage
diff options
context:
space:
mode:
authorNguyễn Gia Phong <mcsinyx@disroot.org>2022-01-09 21:47:22 +0700
committerNguyễn Gia Phong <mcsinyx@disroot.org>2022-01-09 21:47:22 +0700
commit9fd639eff5e47e8e15776f1974f0fcb9337b12f6 (patch)
tree908b0f776a2b6cccf6ff8ef3a6643d7a7d3cc39f
parentcb35d1b5811aac349fd4d09bc3c0d666bd7ebeae (diff)
downloadsite-9fd639eff5e47e8e15776f1974f0fcb9337b12f6.tar.gz
Offically introduce commenting
-rw-r--r--_assets/format.jpgbin0 -> 399407 bytes
-rw-r--r--_assets/formbox.svg72
-rw-r--r--_assets/html5-js.pngbin0 -> 94767 bytes
-rw-r--r--_layout/page_foot.html5
-rw-r--r--_libs/formbox/comment.xml4
-rw-r--r--blog/butter.md2
-rw-r--r--blog/reply.md373
-rw-r--r--blog/teredo.md2
-rw-r--r--blog/threa.md2
-rw-r--r--config.md3
-rw-r--r--utils.jl6
11 files changed, 458 insertions, 11 deletions
diff --git a/_assets/format.jpg b/_assets/format.jpg
new file mode 100644
index 0000000..92fcb07
--- /dev/null
+++ b/_assets/format.jpg
Binary files differdiff --git a/_assets/formbox.svg b/_assets/formbox.svg
new file mode 100644
index 0000000..2942811
--- /dev/null
+++ b/_assets/formbox.svg
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="525px" preserveAspectRatio="none" style="width:523px;height:525px;background:#00000000;" version="1.1" viewBox="0 0 523 525" width="523px" zoomAndPan="magnify"><defs><filter height="300%" id="fi9sg5rwqk4bj" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><!--MD5=[20900bf094bc9067c2447119f2b68668]
+cluster ssg--><rect fill="#FEFECE" filter="url(#fi9sg5rwqk4bj)" height="451" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="504" x="7" y="44.8984"/><rect height="416.375" rx="12.5" ry="12.5" style="stroke:#00000000;stroke-width:1.0;fill:none;" width="498" x="10" y="76.5234"/><line style="stroke:#A80036;stroke-width:1.5;fill:none;" x1="7" x2="511" y1="73.5234" y2="73.5234"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="156" x="181" y="64.75">static site generator</text><!--MD5=[e14ae8005aab70dc5a1b18be5a158da1]
+cluster formbox--><rect fill="#FEFECE" filter="url(#fi9sg5rwqk4bj)" height="183" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="188" x="307" y="239.8984"/><rect height="148.375" rx="12.5" ry="12.5" style="stroke:#00000000;stroke-width:1.0;fill:none;" width="182" x="310" y="271.5234"/><line style="stroke:#A80036;stroke-width:1.5;fill:none;" x1="307" x2="495" y1="268.5234" y2="268.5234"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="156" x="323" y="259.75">comment generator</text><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="105" x="120.5" y="23.125">article source</text><ellipse cx="173" cy="44.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="65" x="348.5" y="23.125">mail box</text><ellipse cx="381" cy="44.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><rect fill="#FEFECE" filter="url(#fi9sg5rwqk4bj)" height="40" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="127" x="208.5" y="118.8984"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="117" x="213.5" y="144.4375">article identifier</text><rect fill="#FEFECE" filter="url(#fi9sg5rwqk4bj)" height="40" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="93" x="206.5" y="219.8984"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="83" x="211.5" y="245.4375">mailto URL</text><rect fill="#FEFECE" filter="url(#fi9sg5rwqk4bj)" height="40" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="121" x="52.5" y="118.8984"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="111" x="57.5" y="144.4375">article content</text><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="113" x="334.5" y="516.75">comment feed</text><ellipse cx="391" cy="495.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><line style="stroke:#A80036;stroke-width:1.5;" x1="395.3891" x2="387.6109" y1="500.2875" y2="492.5094"/><line style="stroke:#A80036;stroke-width:1.5;" x1="395.3891" x2="387.6109" y1="492.5094" y2="500.2875"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="75" x="215.5" y="516.75">web page</text><ellipse cx="253" cy="495.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><line style="stroke:#A80036;stroke-width:1.5;" x1="257.3891" x2="249.6109" y1="500.2875" y2="492.5094"/><line style="stroke:#A80036;stroke-width:1.5;" x1="257.3891" x2="249.6109" y1="492.5094" y2="500.2875"/><rect fill="#FEFECE" filter="url(#fi9sg5rwqk4bj)" height="40" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="119" x="52.5" y="219.8984"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="109" x="57.5" y="245.4375">web feed item</text><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="71" x="82.5" y="516.75">web feed</text><ellipse cx="118" cy="495.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><line style="stroke:#A80036;stroke-width:1.5;" x1="122.3891" x2="114.6109" y1="500.2875" y2="492.5094"/><line style="stroke:#A80036;stroke-width:1.5;" x1="122.3891" x2="114.6109" y1="492.5094" y2="500.2875"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="0" x="383.5" y="218.125"/><ellipse cx="381" cy="239.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><rect fill="#FEFECE" filter="url(#fi9sg5rwqk4bj)" height="40" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="136" x="323" y="346.8984"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="126" x="328" y="372.4375">comment forest</text><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="43" x="326.5" y="443.75">HTML</text><ellipse cx="348" cy="422.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><line style="stroke:#A80036;stroke-width:1.5;" x1="352.3891" x2="344.6109" y1="427.2875" y2="419.5094"/><line style="stroke:#A80036;stroke-width:1.5;" x1="352.3891" x2="344.6109" y1="419.5094" y2="427.2875"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="31" x="375.5" y="443.75">RSS</text><ellipse cx="391" cy="422.8984" fill="#FEFECE" rx="6" ry="6" style="stroke:#A80036;stroke-width:1.5;"/><line style="stroke:#A80036;stroke-width:1.5;" x1="395.3891" x2="387.6109" y1="427.2875" y2="419.5094"/><line style="stroke:#A80036;stroke-width:1.5;" x1="395.3891" x2="387.6109" y1="419.5094" y2="427.2875"/><!--MD5=[dff8c37af403466f05b27b9b18e2c43b]
+link src to content--><path d="M170.11,50.3384 C162.69,61.7084 142.58,92.5484 128.33,114.3984 " fill="none" id="src-to-content" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="125.56,118.6384,133.8385,113.3031,128.3005,114.4564,127.1472,108.9183,125.56,118.6384" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[97ee4fc0d7e327ddb76ae7fcef148ff0]
+link src to id--><path d="M177.25,49.8484 C189.2,60.9484 223.73,93.0384 247.63,115.2484 " fill="none" id="src-to-id" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="251.37,118.7284,247.521,109.6628,247.7152,115.3163,242.0617,115.5105,251.37,118.7284" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[49e00181a3d1a328d6e86a4e1a40b2f5]
+link id to mailto--><path d="M268.34,158.9884 C265.29,174.8784 260.93,197.5884 257.64,214.7384 " fill="none" id="id-to-mailto" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="256.65,219.8684,262.2785,211.7864,257.5948,214.9585,254.4226,210.2747,256.65,219.8684" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[1bd28600502ca0d69f16fa7a57685f76]
+link cin to forest--><path d="M381.4,245.9484 C382.68,261.8384 386.65,311.5084 389.07,341.7184 " fill="none" id="cin-to-forest" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="389.47,346.7584,392.7353,337.4666,389.069,341.7745,384.7611,338.1082,389.47,346.7584" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#A52A2A" font-family="inherit" font-size="16" lengthAdjust="spacing" textLength="56" x="387" y="305.75">extract</text><!--MD5=[976a919b49ab7ea296efe5c55cb948c5]
+link forest to nest--><path d="M375.72,387.0884 C368.5,396.1484 360.26,406.4984 354.65,413.5484 " fill="none" id="forest-to-nest" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="351.24,417.8384,359.9736,413.286,354.353,413.9257,353.7133,408.3052,351.24,417.8384" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[d6a268ea5e1bab6b4d4a511fadfa60d0]
+link forest to wfw--><path d="M391,387.0884 C391,395.2384 391,404.4184 391,411.3084 " fill="none" id="forest-to-wfw" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="391,416.4784,395,407.4784,391,411.4784,387,407.4784,391,416.4784" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[8b51543410ee0ee2e29e0f96c2f90884]
+link mbox to cin--><path d="M381,51.0584 C381,76.8984 381,192.3784 381,228.2384 " fill="none" id="mbox-to-cin" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="381,233.6284,385,224.6284,381,228.6284,377,224.6284,381,233.6284" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[1d6afcb708b5958d97a2b059cf6c9197]
+link id to cin--><path d="M289.71,159.0384 C300.06,169.3184 313.85,181.5684 328,189.8984 C345.44,200.1684 357.05,190.2384 371,204.8984 C377.08,211.2884 379.49,221.1984 380.43,228.6684 " fill="none" id="id-to-cin" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="380.9,233.8084,384.0615,224.4808,380.4435,228.8293,376.0949,225.2113,380.9,233.8084" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[4322aba3ef4de0e332803ac92497ed1f]
+link wfw to comment--><path d="M391,429.0084 C391,440.6684 391,469.4584 391,484.8384 " fill="none" id="wfw-to-comment" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="391,489.8384,395,480.8384,391,484.8384,387,480.8384,391,489.8384" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[1310551b290a55e3b5a4adb4103bcc5d]
+link nest to doc--><path d="M346.56,428.8784 C345.04,433.4684 342.26,440.2584 338,444.8984 C316.2,468.6884 280.42,484.5584 263.06,491.2684 " fill="none" id="nest-to-doc" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="258.31,493.0484,268.142,493.6238,262.9899,491.2881,265.3255,486.136,258.31,493.0484" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[8bf0312297f108d3419df6331699b45a]
+link mailto to doc--><path d="M253,259.9884 C253,310.3684 253,445.6684 253,484.4084 " fill="none" id="mailto-to-doc" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="253,489.7684,257,480.7684,253,484.7684,249,480.7684,253,489.7684" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[a7572903313abee922848dda654f42f4]
+link content to doc--><path d="M78.35,158.9484 C61.97,170.0084 44.05,185.5784 35,204.8984 C24.63,227.0384 26.07,237.1384 35,259.8984 C79.56,373.4584 207.66,464.8584 243.55,488.7684 " fill="none" id="content-to-doc" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="248.16,491.8084,242.8376,483.5216,243.9822,489.0614,238.4424,490.206,248.16,491.8084" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[fe9be14df9bb3783d34b10965073163d]
+link content to item--><path d="M112.81,158.9884 C112.65,174.8784 112.42,197.5884 112.24,214.7384 " fill="none" id="content-to-item" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="112.19,219.8684,116.2926,210.9147,112.2471,214.8688,108.2931,210.8233,112.19,219.8684" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[5ab0a57b656787d49f1483879c7b5fe8]
+link mailto to item--><path d="M206.35,239.8984 C196.53,239.8984 186.71,239.8984 176.89,239.8984 " fill="none" id="mailto-to-item" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="171.76,239.8984,180.76,243.8984,176.76,239.8984,180.76,235.8984,171.76,239.8984" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[0078ca97dc9240dbef47ed7f72ac16f6]
+link item to feed--><path d="M112.45,259.9884 C113.64,310.3684 116.84,445.6684 117.75,484.4084 " fill="none" id="item-to-feed" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="117.88,489.7684,121.6733,480.6794,117.7657,484.7697,113.6754,480.8622,117.88,489.7684" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[16177b3991164ff9b83ca2a609c4276f]
+@startuml

+hide empty description

+skinparam backgroundColor transparent

+skinparam defaultFontColor Brown

+skinparam defaultFontName inherit

+skinparam defaultFontSize 16

+

+state "static site generator" as ssg {

+  state "article source" as src <<entryPoint>>

+  state "mail box" as mbox <<entryPoint>>

+  state "article identifier" as id

+  state "mailto URL" as mailto

+

+  src -> content

+  src -> id

+  id - -> mailto

+

+  state "comment generator" as formbox {

+    state " " as cin <<entryPoint>>

+    state "comment forest" as forest

+    state "HTML" as nest <<exitPoint>>

+    state "RSS" as wfw <<exitPoint>>

+    cin - -> forest : extract

+    forest -> nest

+    forest -> wfw

+  }

+

+  mbox -> cin

+  id - -> cin

+

+  state "comment feed" as comment <<exitPoint>>

+  state "article content" as content

+  state "web page" as doc <<exitPoint>>

+  state "web feed item" as item

+  state "web feed" as feed <<exitPoint>>

+

+  wfw - -> comment

+  nest -> doc

+  mailto -> doc

+  content -> doc

+  content - -> item

+  mailto -> item

+  item - -> feed

+}

+@enduml

+
+PlantUML version 1.2021.16(Thu Dec 09 00:25:22 ICT 2021)
+(GPL source distribution)
+Java Runtime: OpenJDK Runtime Environment
+JVM: OpenJDK 64-Bit Server VM
+Default Encoding: UTF-8
+Language: en
+Country: US
+--></g></svg>
\ No newline at end of file
diff --git a/_assets/html5-js.png b/_assets/html5-js.png
new file mode 100644
index 0000000..37e6219
--- /dev/null
+++ b/_assets/html5-js.png
Binary files differdiff --git a/_layout/page_foot.html b/_layout/page_foot.html
index bb10e5a..3ec6f70 100644
--- a/_layout/page_foot.html
+++ b/_layout/page_foot.html
@@ -6,7 +6,6 @@
 
 {{isnotempty rss}}<h2>Comments</h2>
 {{comments_rendered}}
-<p>Follow the anchor in an author's name to reply via
-  <a href=https://useplaintext.email>plaintext email</a>.  Markdown
-  inline markups, block quotes, lists and code blocks are supported.</p>{{end}}
+<p>Follow the anchor in an author's name to reply.  Please read
+  the <a href=/blog/reply#moderation>rules</a> before commenting.</p>{{end}}
 {{insert footer.html}}
diff --git a/_libs/formbox/comment.xml b/_libs/formbox/comment.xml
index 5f183a0..524e0c7 100644
--- a/_libs/formbox/comment.xml
+++ b/_libs/formbox/comment.xml
@@ -4,6 +4,8 @@
   <pubDate>{date}</pubDate>
   <dc:creator>{author}</dc:creator>
   <title>On {date}, {author} wrote:</title>
-  <content:encoded><![CDATA[{body}]]></content:encoded>
+  <content:encoded><![CDATA[{body}<br>
+    <a href="mailto:~cnx/site@lists.sr.ht?{mailto_params}">Reply
+      via email</a>]]></content:encoded>
 </item>
 {children}
diff --git a/blog/butter.md b/blog/butter.md
index ec1af10..1009273 100644
--- a/blog/butter.md
+++ b/blog/butter.md
@@ -2,7 +2,7 @@
 rss = "How I reinstalled NixOS on Btrfs with an amnesiac root
        and backed up my data"
 date = Date(2021, 11, 14)
-tags = ["backup", "btrfs", "fun", "nixos"]
+tags = ["backup", "btrfs", "fun", "nixos", "recipe"]
 +++
 
 # NixOS on Btrfs+tmpfs
diff --git a/blog/reply.md b/blog/reply.md
new file mode 100644
index 0000000..609ab97
--- /dev/null
+++ b/blog/reply.md
@@ -0,0 +1,373 @@
++++
+rss = "Comments for Static Sites without JavaScript via Emails"
+date = Date(2022, 1, 9)
+tags = ["fun", "recipe"]
++++
+
+# Comments for Static Sites without JavaScripts
+
+> I'm open for criticism\
+> But really, is it any room for criticism?
+
+Recently, I've switched my [feed] reader from [Newsboat] to [Liferea].
+The latter has a GUI and some extra features which make the experience
+a lot more comfy.  For instance, custom enclosure handling lets me
+to finally migrate all of my YouTube subscriptions to [Atom] and *conveniently*
+browse and watch videos using [mpv].  Image support also allows me
+to directly view web comics.[^image]  One of them, [The Monster Under
+the Bed][TMUTB],[^nsfw] does not embed the strips in its feed, but it
+has comments.
+
+Yes, [RSS] includes support for `<comments>`, and I was not aware of it
+until [very recently][spark].  I suppose many other people late to
+the (web feed) party are neither.  Since the rise of static sites,
+feeds have regain popularity, even for [Google to reconsider
+its direction][android].  Compare to RSS or Atom, alternatives have
+the following shortcomings:
+
+* [Usenet] is generally obsolete to most people.
+* [Mailing list] messages are immutable.
+* Fora and social media are silos.[^silo]
+* Social media are designed for ephemeral discussions.
+* Instant messaging is awful for archival.
+
+On the other hand, news feeds are commonly read-only: only a few readers
+can render comments and even fewer are able to post one.  On the server side,
+a dynamic server is needed to accept comments.  Traditionally, it's the same
+as the system serving the website.  Although this works, it is significantly
+more costly than a server dedicated to static sites, which scale a lot better.
+
+[Hackers] have came up with multiple workarounds such as using [microblogging]
+or [instant messaging][cactus] to add comments to their static sites,
+but all require client-side code execution, which is an option for neither RSS
+nor Atom.  Furthermore, [JavaScript hurts portability and performance][curlpit]
+on the WWW, hence it should be avoided unless it is absolutely impossible
+to implement a feature otherwise.  Commenting is not an exception.
+
+Following is my adventure implementing a comment section for this very blog.
+If you're also up to the task, I think you should view what I did
+as an inspiration (rather than a reference) and don't be afraid
+to experiment around until satisfaction.
+
+\toc
+
+## Choosing Back-End
+
+As mentioned earlier, static sites or not, there still needs to be
+a dynamic component to accept incoming replies.  HTTP requests would be
+the most portable since all netizen obviously have a web browser, but those
+are what we're trying to replace here.  What else does everyone has nowadays?
+Something so common that it can be used to identify people upon
+service registrations?  Exactly, emails and phone numbers!
+
+OK, Imma stop horsing around.  My back-end of choice would be emails.
+It's global, it's cheap and federated.  Cellular services almost fit the bill,
+except that they would cost an arm and leg for one to comment around the web
+everyday via SMS, whose character limit is not facilitating thoughtful
+discussions either.  As for forum, social medium or instant messaging,
+no platform has nearly as large of an user base as electronic mails.
+
+![HTML is often a trojan horse for JavaScript](/assets/html5-js.png)
+
+It's not like any email would fit the comment section though.  Especially
+not the HTML kind with a few hundred kilobytes of embedded CSS, JS
+and non-content images.  From the security standpoint alone 'tis already
+a no-go.  A light markup language like Markdown[^mime] would be much better.
+
+One great thing about using a mature technology like email is that we have
+all use cases covered.  Filtering, exporting and parsing emails work out-of-box
+regardless of one's provider, [MUA] and programming preferences.  I have
+an SourceHut account with which I can create mailing lists on-demand
+so I'm using it; however there's no reason exporting from your private inbox
+is any more difficult, presuming you have set up [offline email].
+
+!!! note "Tips and tricks"
+
+    Speaking of SourceHut, exporting a mailing list archive is rather easy,
+    one could either use the button on the web UI or download from the API.
+    As the operation is not exactly cost-free, the former is protected
+    by a [CSRF] token and the latter by [OAuth 2.0].  If you are a fellow
+    [sr.ht] user, you can use [acurl] on the build service with the URL
+    from the [GraphQL] `query { me { lists { results { name, archive } } } }`.
+
+## Designing Data Flow
+
+I promise, this sounds bigger than it really is, but first,
+let's have a glance at how static generators work.  Typically,
+there are three times templating happens:
+
+1. Conversion of individual articles into HTML *content*
+2. Inserting each article content in a page template
+   to create a complete HTML document
+3. Inserting multiple HTML contents into one RSS or Atom feed template
+
+At completion, two kinds of output are generated: website and web feed.
+Similarly, comments have to be rendered for both targets: an HTML
+comment section for web browsing and a separate RSS feed for each article's
+`<wfw:commentRss>`.[^wfw]  Therefore, injections should be done separately
+at stage 2 and 3.  The overall process of static site generation
+with email comments is illustrated as follows.
+
+![Data transformation during generation process](/assets/formbox.svg)
+
+For clarity, HTML and RSS input templates for comments and their parent page
+and web feed are omitted.  Path to each *comment feed* output being injected
+in the respective *web feed item* is also not shown in the figure.
+
+## Implementation
+
+At the time of writing, this personal website of mine was generated
+by [Julia] [Franklin], who was neither fast[^speed] nor [semantic],
+but was the only one I knew supporting LaTeX prerendering out of the box.
+Franklin is also rather [extendable] via Julia functions.
+
+### Accepting Replies
+
+Let's start with how each article can be programmatically and uniquely
+identified.  By default in RSS, a [GUID][^guid] is the permanent URL
+of the associated web page.  I am not exactly a creative person, so I mirrored
+this idea, although I only used the difference between URLs, i.e. minus
+the scheme, network location and trailing `index.html` (Franklin always
+appends it to the target path of any source file that is neither `index.md`
+nor `index.html`):
+
+```julia
+dir_url() = strip(dirname(locvar(:fd_url)), '/')
+message_id() = "%3C$(dir_url())@cnx%3E"
+```
+
+For maximum portability, threading identification is used in emails'
+`In-Reply-To` header, which expects a message ID, which must match
+`<.+@.+>`.  Once again, to avoid having to think, I opted for
+the path difference for the left hand side and my nickname `cnx`
+for the right.  The `mailto` URI could be then be constructed accordingly:
+
+```julia
+using Printf: @sprintf
+
+function hfun_mailto_comment()
+  @sprintf("mailto:%s?%s=%s&%s=Re: %s",
+           "~cnx/site@lists.sr.ht",
+           "In-Reply-To", message_id(),
+           "Subject", locvar(:title))
+end
+```
+
+The anchor was then added to the page foot:
+
+```html
+<a href="{{mailto_comment}}"
+   title="Reply via email">{{author}}</a>
+```
+
+### Rendering Comments
+
+This is when the fun begins.  Julia's standard library does not include
+an email parser, and I doubt your favorite language does either,
+unless it is named after a British comedy troupe.  Python is often described
+as *batteries included*, or at least it used to (seemingly the consensus among
+current core devs has shifted towards [favoring third-party libraries][3rd]).
+
+!!! note "Off-topic rambling"
+
+    Standard library inclusion wasn't really the deal breaker here though.
+    I still needed a Markdown engine and a HTML sanitizer (because Markdown
+    can include HTML), and AFAICT no stdlib has them.  The read issue was
+    with the lack of Julia packaging on most distributions (apart from Guix),
+    and most certainly [not on NixOS], my current distro.  For the same reason
+    the idea of rewriting Franklin in Python has been running in my head
+    for a while now.  Python packaging is much more downstream-friendly
+    and unlike Julia compilation overhead is almost non-existent.
+
+On the other hand, it's trivial to pipe an external program's output to Julia,
+e.g. ``readchomp(`echo foo bar`)`` would give you the string "foo bar".  Thus,
+the to-be-written *comment generator* should take (the path to) a mail box,
+the message ID of the article and a template, and write the result to stdout.
+Argument parsing is, again, thankfully in Python's stdlib:
+
+```python
+from argparse import ArgumentParser
+from pathlib import Path
+from urllib.parse import unquote
+
+parser = ArgumentParser()
+parser.add_argument('mbox')
+parser.add_argument('id', type=unquote)
+parser.add_argument('template', type=Path)
+args = parser.parse_args()
+```
+
+I then parsed the [mbox] into a mapping indexed by parent message IDs
+as follows.  They would be HTML-unquoted so that was why I needed
+to do the same for the input message ID.
+
+```python
+from collections import defaultdict
+from email.utils import parsedate_to_datetime
+from mailbox import mbox
+
+date = lambda m: parsedate_to_datetime(m['Date']).date()
+archive = defaultdict(list)
+for message in sorted(mbox(args.mbox), key=date):
+    archive[message['In-Reply-To']].append(message)
+```
+
+As said earlier, arbitrary HTML content is not exactly suitable for comments.
+However, it is undeniable that HTML emails have taken over the world
+and compromises must be made: allowing `multipart/alternative` of both
+`text/plain` and `text/html`.  It is not the only multipart, so are
+attachments and cryptographic signatures.  Since we are only interested
+in the plaintext part, it is actually easier done than said to extract it:
+
+```python
+from bleach import clean, linkify
+from markdown import markdown
+
+def get_body(message):
+    if message.is_multipart():
+        for payload in map(get_body, message.get_payload()):
+            if payload is not None: return payload
+    elif message.get_content_type() == 'text/plain':
+        body = message.get_payload(decode=True)
+        return clean(linkify(body, output_format='html5')),
+                     tags=..., protocols=...)
+    return None
+```
+
+Now all that's left is to render that body and relevant headers
+as an HTML segment or an RSS item.  This is when we revisit the template.
+Jinja is probably the most popular in Python, thanks to Django and Flask,
+but its complexity is rather unnecessary.  Instead, I went with the built-in
+`str.format`.
+
+![Double braces are brilliant, but I prefer single ones](/assets/format.jpg)
+
+What are templates for, exactly?  Not the complete document, apparently,
+because that would differs from article to article and increase the complexity
+for injection.  Neither a single comment, as comments are threaded into trees
+(or a forest) and their relationship can be useful.  We gotta [meet
+in tha middle] and use recursive templates instead, e.g. for nested comments:
+
+```html
+<div class=comment>
+  ...
+  {children}
+</div>
+```
+
+To render linear comments, such as for `<wfw:commentRss>`, simply move
+the children out of the item as follows.
+
+```xml
+<item>
+  ...
+</item>
+{children}
+```
+
+The rest substitutions are mostly just extracted from the email's headers.
+Another bit that needs some extra decisions, though, is the parameters
+for the `mailto` URI to reply to each comment:
+
+* `In-Reply-To` set to current `Message-Id`
+* `Cc` set to current `Reply-To` (if exists) or `From`
+* `Subject` is inherited, with `Re:` prepended if missing
+
+This is getting boring with a lot of trivial code, so I'll leave you
+with a pointer to the completed script named [formbox] and move on
+to more interesting stuff.
+
+### Injecting Comments
+
+Inserting HTML comment sections is pretty simple.  First I wrote a simple
+Julia function `render_comments` calling `formbox` under the hood, then
+
+```julia
+hfun_comments_rendered() = render_comments("comment.html")
+```
+
+`comments_rendered` is then injected below the article.  For RSS,
+it took an extra steps:
+
+1. Insert `render_comments("comment.xml")` to the comment feed template
+   `comments.xml` (notice they are two different templates) and write it
+   next to the article's output `index.html`
+2. Insert the path of the written comment feed to the `<wfw:commentRss>` tag
+   in the article's feed item
+
+That's it!
+
+## Moderation
+
+I don't want a *Terms of Services* page, it'd feel too corporate
+for my *personal* website, so I will list the rules here:
+
+1. Please be excellent to each other.  Disagreements are okay,
+   personal insults are not.
+2. Stay on topic.  If you want to publicly discuss with me
+   about something else, start a new thread on a [mailing list]
+   or reach me via social media.
+3. [Use plaintext emails] and do not top post.  Markdown inline markups,
+   block quotes, lists and code blocks are supported.
+4. Comments are implied to be under [CC BY-SA 4.0] unless declared otherwise.
+5. I reserve the right to remove any comment I don't like.
+   I generally don't delete comments, but if you want to exercise
+   your freedom of speech, publish it yourself.
+6. I do not warrant the availability of the comments either.
+   I will try my best but one day all comments may just disappear,
+   just like this website itself.  Archive what you deem important.
+7. These rules are subject to change according to my personal liking
+   without notice.
+
+Replies will only be rendered on the website and feed after I see them,
+so please expect a delay of at least 24 hours.  If you are eager to reply
+to each other, subscribe to the [site's mailing list] instead.
+
+[^image]: TBF there are image preview scripts in Newsboat's [contrib].
+[^nsfw]: Content warning: occasionally NSFW
+[^silo]: Federation is getting there for social media; not so much for fora.
+[^mime]: But don't use [text/markdown] for your emails.
+[^wfw]: Unfortunately there's no equivalence for Atom.
+[^speed]: Over 30 seconds to generate a few hundred kB of web pages.
+[^guid]: Not to be confused with the micro soft hijacked term for [UUID].
+
+[feed]: https://en.wikipedia.org/wiki/Web_feed
+[Newsboat]: https://newsboat.org
+[Liferea]: https://lzone.de/liferea
+[Atom]: https://en.wikipedia.org/wiki/Atom_(Web_standard)
+[mpv]: https://mpv.io
+[TMUTB]: https://themonsterunderthebed.net
+[RSS]: https://www.rssboard.org/rss-specification
+[spark]: https://nixnet.social/notice/AEO3fYbuzYCJl85eD2
+[android]: https://www.theregister.com/2021/05/20/google_rss_chrome_android
+[Mailing list]: https://en.wikipedia.org/wiki/Mailing_list
+[Usenet]: https://en.wikipedia.org/wiki/Usenet
+[Hackers]: https://en.wikipedia.org/wiki/Hacker
+[microblogging]: https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon
+[cactus]: https://cactus.chat
+[curlpit]: https://unixsheikh.com/articles/so-called-modern-web-developers-are-the-culprits.html
+[MUA]: https://en.wikipedia.org/wiki/Email_client
+[offline email]: https://drewdevault.com/2021/05/17/aerc-with-mbsync-postfix.html
+[CSRF]: https://en.wikipedia.org/wiki/Cross-site_request_forgery
+[OAuth 2.0]: https://man.sr.ht/meta.sr.ht/oauth.md
+[sr.ht]: https://sr.ht
+[acurl]: https://man.sr.ht/builds.sr.ht/manifest.md#tasks
+[GraphQL]: https://lists.sr.ht/graphql
+[wfw]: https://web.archive.org/web/20050301040756/http://www.sellsbrothers.com/spout/#exposingRssComments
+[Julia]: https://julialang.org
+[Franklin]: https://franklinjl.org
+[semantic]: https://github.com/tlienart/Franklin.jl/issues/936
+[extendable]: https://franklinjl.org/syntax/utils
+[GUID]: https://www.rssboard.org/rss-profile#element-channel-item-guid
+[3rd]: https://discuss.python.org/t/adopting-recommending-a-toml-parser/4068
+[not on NixOS]: https://github.com/NixOS/nixpkgs/issues/20649
+[mbox]: https://datatracker.ietf.org/doc/html/rfc4155
+[meet in tha middle]: https://genius.com/Timbaland-meet-in-tha-middle-lyrics
+[formbox]: https://sr.ht/~cnx/formbox
+[Use plaintext emails]: https://useplaintext.email
+[mailing list]: https://lists.sr.ht/~cnx/misc
+[CC BY-SA 4.0]: https://creativecommons.org/licenses/by-sa/4.0
+[site's mailing list]: https://lists.sr.ht/~cnx/site
+[contrib]: https://drewdevault.com/2020/06/06/Add-a-contrib-directory.html
+[text/markdown]: https://blog.brixit.nl/markdown-email
+[UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier
diff --git a/blog/teredo.md b/blog/teredo.md
index d283977..0140f4d 100644
--- a/blog/teredo.md
+++ b/blog/teredo.md
@@ -1,7 +1,7 @@
 +++
 rss = "Teredo tunnel simulation in virtual machines"
 date = Date(2020, 7, 3)
-tags = ["fun", "ipv6", "tunnel"]
+tags = ["fun", "ipv6", "recipe"]
 +++
 
 # Teredo Tunnel Simulation
diff --git a/blog/threa.md b/blog/threa.md
index 4694a42..e0c9d70 100644
--- a/blog/threa.md
+++ b/blog/threa.md
@@ -1,7 +1,7 @@
 +++
 rss = "Raku's concision demonstrated in form of a tutorial"
 date = Date(2021, 7, 3)
-tags = ["clipboard", "fun", "raku"]
+tags = ["clipboard", "fun", "raku", "recipe"]
 +++
 
 # Writing a Clipboard Manager
diff --git a/config.md b/config.md
index a77b1f7..60ef4fe 100644
--- a/config.md
+++ b/config.md
@@ -2,8 +2,9 @@
 author = "Nguyễn Gia Phong"
 website_title = "Web logs by McSinyx"
 website_description = "Random write-ups packed with pop culture references"
-copyright = "🄯 2019–2021 " * author
+copyright = "🄯 2019–2022 " * author
 website_url = "https://cnx.srht.site"
+website_url = "http://localhost:8000"
 date_format = "yyyy-mm-dd"
 mintoclevel = 2
 generate_rss = true
diff --git a/utils.jl b/utils.jl
index 9c12c25..f1cba1d 100644
--- a/utils.jl
+++ b/utils.jl
@@ -23,17 +23,17 @@ function render_comments(template)
 end
 
 hfun_comments_rendered() = render_comments("comment.html")
+hfun_comment_rss_feed_url() = joinpath(dirname(locvar(:fd_full_url)),
+                                       "comments.xml")
 
 function hfun_comment_rss()
   rpath = joinpath(dir_url(), "comments.xml")
   open(joinpath(path(:site), rpath), "w") do feed
     write(feed, convert_html(readchomp(joinpath(path(:rss), "comments.xml"))))
   end
-  joinpath(globvar(:website_url), rpath)
+  hfun_comment_rss_feed_url()
 end
 
-hfun_comment_rss_feed_url() = joinpath(dirname(locvar(:fd_full_url)),
-                                       "comments.xml")
 hfun_comment_rss_items() = render_comments("comment.xml")
 
 function hfun_fediring(args)