Compare commits

..

3 Commits

Author SHA1 Message Date
Shadowfacts 6843df46f0 Add env var to disable outbound federation 2023-01-05 15:58:20 -05:00
Shadowfacts 0884fe7589 Assorted comment tweaks 2023-01-05 15:58:08 -05:00
Shadowfacts ee04ed9124 Add Rewritten in Rust 2023-01-05 15:02:31 -05:00
7 changed files with 92 additions and 16 deletions

View File

@ -238,6 +238,16 @@ article {
// and padding for horizontal so that the space is between the border and the text
margin: 20px 0;
padding: 0 40px;
&.pull {
margin: 0 10px;
padding: 0 20px;
width: 40%;
font-size: 1.15rem;
&.left { float: left; }
&.right { float: right; }
}
}
p code {
@ -427,17 +437,28 @@ article {
margin-bottom: 0;
}
.comment:not(:last-child) {
.comment {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
&:not(:last-child) {
margin-bottom: 16px;
}
.comment-user-avatar {
width: 50px;
border-radius: 5px;
float: left;
margin: 0;
margin-right: 10px;
}
.comment-body > p:first-child {
margin-top: 0;
}
}
@media (min-width: 768px) {
.comments-list {
padding-left: 0px;
@ -453,7 +474,6 @@ article {
}
.comment-children {
margin-left: 60px;
margin-top: 16px;
}
}

View File

@ -69,7 +69,8 @@
</article>
<script>
const permalink = "{{ post.comments_permalink() }}";
const articleDomain = "{{ Self::domain() }}";
const articlePermalink = "{{ post.comments_permalink() }}";
</script>
<script src="/js/comments.js" async></script>

View File

@ -22,7 +22,7 @@
<meta property="twitter:image" content="https://{{ Self::domain() }}shadowfacts.png">
<meta property="og:image" content="https://{{ Self::domain() }}shadowfacts.png">
{% endblock %}
<meta property="og:url" content="https://{{ Self::domain() }}{ self.permalink() }}">
<meta property="og:url" content="https://{{ Self::domain() }}{{ self.permalink() }}">
<meta property="og:site_name" content="Shadowfacts">
{% block head %}{% endblock %}

View File

@ -42,7 +42,7 @@ The site remains almost entirely JavaScript free. There are two places where cli
## The Backend
The previous version of my website used [Jekyll](https://jekyllrb.com/) (and WordPress before that, and Jekyll again before that). In what may become a pattern, I've once more switched away from Jekyll. Version Four uses something completely custom. It has been a work-in-progress in one form or another for about a year now. It started out as a Node.js project that was going to be a general-purpose static site generator. Then, around the time I was learning Elixir (which I love, and will be the subject of another blog post), I attempted to rewrite it in that[^3]. Then we finally arrive at the current iteration of the current iteration of my website. In spite of my distaste for the ecosystem[^4], I returned to Node.js. This time, however, the project took a bit of a different direction than the previous two attempts at a rewrite. It has two main parts: the static site generator and the ActivityPub integration.
The previous version of my website used [Jekyll](https://jekyllrb.com/) (and WordPress before that, and Jekyll again before that). In what may become a pattern, I've once more switched away from Jekyll. Version Five uses something completely custom. It has been a work-in-progress in one form or another for about a year now. It started out as a Node.js project that was going to be a general-purpose static site generator. Then, around the time I was learning Elixir (which I love, and will be the subject of another blog post), I attempted to rewrite it in that[^3]. Then we finally arrive at the current iteration of the current iteration of my website. In spite of my distaste for the ecosystem[^4], I returned to Node.js. This time, however, the project took a bit of a different direction than the previous two attempts at a rewrite. It has two main parts: the static site generator and the ActivityPub integration.
[^3]: Unfortunately, this attempt ran into some issues fairly quickly. Elixir itself is wonderful, but the package ecosystem for web-related things such as Sass, Markdown rendering, and syntax highlighting, is lackluster.

View File

@ -0,0 +1,50 @@
```
title = "Rewritten in Rust"
tags = ["meta"]
date = "2023-01-05 14:30:42 -0500"
short_desc = "If you're reading this message, I'm being held hostage by the Rust Evangelism Strike Force at—"
slug = "rewritten-in-rust"
```
So, about six months ago I decided I wanted to rewrite my perfectly-working blog backend in Rust. Why? Because I was bored and wanted an excuse to use Rust more.
<!-- excerpt-end -->
The fundamental architecture of my site is unchanged from the last [rewrite](/2019/reincarnation). All of the HTML pages are generated up front and written to disk. The HTTP server can then handle any ActivityPub-specific requests and fall back to serving files straight from disk.
<blockquote class="pull right">
i look forward to finishing this rewrite and then being able to sit back and enjoy... *checks notes* the exact same website i had before
</blockquote>
Because this project was undertaken with the deliberate goal of using Rust more, I let myself spend more time bikeshedding and working on pieces that I ordinarily would have ignored or left to 3rd party libraries. One of those was spending probably too much time writing a bunch of code to slugify post titles—that is, turning titles with lots of punctuation and things into a nice and URL-safe format. It handles a bunch of pet peeves I have when I look at URLs on other websites (e.g., non-ASCII characters geting blindly replaced resulting in long sequences of hyphens), even if those are highly unlikely to ever arise here.
Another component I spent a great deal of time working on was the Markdown to HTML rendering. I'm using the [pulldown-cmark](https://lib.rs/crates/pulldown-cmark) crate, which handles a great deal for me, but not quite everything. In the previous implementation of my blog, I was using the [markdown-it](https://github.com/markdown-it/markdown-it) package which has some other little niceties to make the generated HTML better, in addition to the custom plugins I was using for the Markdown decorations you see if you're reading this on my blog itself. I have custom code for handling the link and heading decorations, as before. I also override how footnote definitions are generated, to make them all appear at the end of the HTML rather appearing in the same locations they're defined in the Markdown. As part of the changes to footnote definitions, I also generate backlinks which go from the footnote back to where it was referenced, to make reading them a bit easier.
## Syntax Highlighting
The previous, Node.js implementation of my blog used highlight.js for syntax highlighting. This works decently well, but it doesn't produce the most accurate highlighting since it's essentially a big pile of regexes rather than parsing the syntax. Now, I'm using [Tree Sitter](https://tree-sitter.github.io/tree-sitter/) which actually parses the language and so highlighting ends up being more accurate.
Unfortunately, software being what it is, this is not always the case. The syntax highlighting for Swift with the best available Tree Sitter grammar is substantially worse than the highlight.js results. It routinely misses keywords, misinterprets variables as functions, and—to top all that off—something about how the highlight query is structured is incredibly slow for Tree Sitter to load. It more than doubles the time it takes to generate my blog, from about 0.5 seconds when skipping Swift highlighting to 1.3s when using tree-sitter-swift.
So, because I've never met a problem I couldn't yak-shave, I decided the solution was to use John Sundell's [Splash](https://github.com/johnsundell/Splash) library for highlighting Swift snippets. A number of Swift blogs I follow use it, and it seems to produce very good results. But, of course, it's written in Swift, so I need some way of accessing it from Rust. This was a little bit of an ordeal, and it ended up being very easy, then very difficult, then easy, then confusing, and finally not too bad. The details of how exactly everything works and what I went through are a subject for another time, but if you want to see how it works, the source code for the Rust/Swift bridge is [available here](https://git.shadowfacts.net/shadowfacts/splash-rs/src/branch/main).
## ActivityPub
One of the big features from the last time I rewrote my blog was the [ActivityPub](https://www.w3.org/TR/activitypub/) integration. Almost nobody uses it, but I think its a cool feature and so I wanted to keep it. I'm using the [activitystreams](https://lib.rs/crates/activitystreams) crate, and what I've learned is that statically typed languages are maybe not so great for writing AP implementations. Lots of properties can be multiple different types, so you can't directly read anything, you have to check that it is in fact the type you expect. This does make for a more correct implementation, but it ends up being a big pain in the ass.
<aside>
ActivityPub and ActivityStreams 2 are both incredibly abstract and generic specifications, so trying to use them from statically typed languages ends up being more than a little bit cumbersome. Unfortunately, writing a relatively complete and widely compatible AP implementation is a complex enough task that writing one in a dynamically typed language is also a bit of a hassle. Maybe the move is to use a dynamically typed language and just have very good test coverage from the get-go.
</aside>
ActivityPub support was undoubtedly the part of this project that took the most time. Just about all of the static generator was complete within a couple weeks. The AP support dragged on over the next 6 months because it was so unpleasant (both for the aforementioned reasons, and just that dealing with all the quirks of different AP implementations is a pain).
But, now that it's all done, I'm pretty happy with where it is. The ActivityPub support is mostly unchanged. Blog posts are still AP `Article` objects that you can interact with and comment on, and you can still follow the blog AP actor from Mastodon/etc. to get new posts to show up in your home feed. I did make a couple small quality-of-life changes:
1. If you reply to a blog post with a non-public post, you'll get an automated reply back telling you that's not supported. The purpose of replying to a blog post is to make comments show up, and I don't wnat to display things that people didn't intend to be public. If you want to send a private comment, you can message me directly, rather than the blog itself.
2. When there are new comments, the blog will automatically send me a notification (via AP, of course) with a digest of new posts. Previously, I had to manually check if there were any comments (if you ever commented and I never noticed it, sorry).
## Misc
The only other notable change is the addition of the [TV](/tv/) section, which is an archive of the various long-running commentary threads I've written on Mastodon.

View File

@ -9,10 +9,10 @@ details.addEventListener("toggle", (even) => {
});
async function fetchComments() {
const res = await fetch(`/comments?id=${permalink}`);
const res = await fetch(`/comments?id=${articlePermalink}`);
const comments = await res.json();
const rootId = new URL(permalink, document.location);
const tree = buildCommentsTree(comments, rootId)[0];
const rootId = new URL(`https://${articleDomain}${articlePermalink}`);
const [tree, _] = buildCommentsTree(comments, rootId);
const html = renderCommentList(tree);
document.getElementById("comments-container").innerHTML += html;
}

View File

@ -4,7 +4,7 @@ use anyhow::anyhow;
use chrono::Utc;
use http_signature_normalization::Config;
use hyper::header::CONTENT_TYPE;
use log::{debug, error};
use log::{debug, error, info};
use openssl::{hash::MessageDigest, pkey::PKey, sha::Sha256, sign::Signer};
use reqwest::header::{ACCEPT, DATE, HOST};
use std::collections::BTreeMap;
@ -25,6 +25,11 @@ pub async fn sign_and_send<Kind: serde::Serialize>(
activity: &ActorAndObject<Kind>,
inbox: &str,
) -> anyhow::Result<()> {
if std::env::var("SKIP_FEDERATE").is_ok() {
info!("Skipping federation");
return Ok(());
}
let inbox_url = Url::parse(inbox)?;
let body = serde_json::to_string(activity)?;