diff options
-rw-r--r-- | _assets/format.jpg | bin | 0 -> 399407 bytes | |||
-rw-r--r-- | _assets/formbox.svg | 72 | ||||
-rw-r--r-- | _assets/html5-js.png | bin | 0 -> 94767 bytes | |||
-rw-r--r-- | _layout/page_foot.html | 5 | ||||
-rw-r--r-- | _libs/formbox/comment.xml | 4 | ||||
-rw-r--r-- | blog/butter.md | 2 | ||||
-rw-r--r-- | blog/reply.md | 373 | ||||
-rw-r--r-- | blog/teredo.md | 2 | ||||
-rw-r--r-- | blog/threa.md | 2 | ||||
-rw-r--r-- | config.md | 3 | ||||
-rw-r--r-- | utils.jl | 6 |
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) |