Hosting a blog on Matrix
If you're reading this, it means it works — this blog is hosted on Matrix. This post describes how I've done it.
One thing about me is that I'm a fan of decentralized network protocols. Decentralized protocols — think DNS, SMTP+IMAP, HTTP — form a strong foundation of the Internet. Also, I have fond memories of using XMPP and its transports to chat with my friends in the early 2010s. This is why I look forward to the future of Matrix, an open protocol for decentralized, real-time network communications.
Another thing about me is that I like to tinker with software and enjoy working on… call it unusual problems. I also like to read about people doing weird cool things with tech, so I figured that maybe it would be cool to have a blog.
Therefore it makes perfect sense that one time during a shower I've got an idea: what if I could combine the two and host a blog on Matrix?
Why would you put a blog in a chat app?
I guess this is likely the first question that comes to mind. It's probably not the best way to host a blog. After all, you can use any hosted blog platform like WordPress or write some Markdown in a Git repository and use a static site generator like Eleventy or Hugo. This would make your life easier.
Well, the true answer is really because I can. It turns out that Matrix is not just a chat protocol. It's more like a decentralized, time-ordered event store that synchronized between federating servers. In other words — if you can shape something as a series of events, you can put it in Matrix.
Of course, Matrix is still used primarily for chat, so I thought that it would be nice to be able to interact with the blog using regular Matrix clients like Element. This way, you could allow people to read the blog posts and comment on them directly in their chat clients. It's just a matter of mapping the blog to a compatible representation.
Putting the pieces together
What do we need to represent a blog? Aside from a list of posts, each post should have:
- a title
- an optional summary
- an URL-safe identifier (also known as a slug)
- publication date
- last edit date
- the post content itself
It turns out that the base protocol already has all the necessary pieces!
First, in Matrix, communication happens in rooms — just like on Slack or IRC. Because I wanted to potentially have discussions on every single blog posts, it makes sense to represent blog posts as rooms.
A room can have a user-friendly name that can represent a post title. It can also have a topic, which usually describes the current conversation topic, but could serve as a summary of the blog post.
Usually, each Matrix room is identified by a server-generated string that looks like this: !xZzBOxfyJgfJbaiEGc:evolved.systems
. While this is perfectly fine to be used in an URL, it's not very readable and it's nice to have readable URLs. Fortunately, rooms can have aliases (called addresses in Element) that solve the readability problem. They look like this: #matrix:matrix.org
. A room can have multiple aliases (e.g. on multiple servers), but there's only one canonical alias — and that's what I use to represent the slug. However, there's one caveat: because I also want to use rooms for other things than a blog, I add a prefix in front of the slug to prevent name clashes. This means that, for example, this blog post would have a canonical alias of #blog.hosting-a-blog-on-matrix:evolved.systems
. Using aliases has a nice bonus of controlling if a post is visible — if there's no alias, it's not "published" and therefore not visible.
Finally, representing the post content sounds pretty easy — just write a message! We can say that the first message in a room will be the post content. Matrix supports plain text and HTML-formatted messages. Both types can be set side-by-side, so you can put Markdown in the plain text part and the rendered HTML in the formatted part, which is pretty convenient.
Everything is an event
So far it's all pretty simple, so I think this is a good time to add some color. It's easy to say "just write a message" or "set a name", but what does this mean?
Well, as I mentioned before, Matrix is really a time-ordered event store. You could say that rooms actually are timelines of events. Furthermore, there are two kinds of events — message events and state events.
Message events are pretty self-explanatory — some content sent by somebody at a given time. On the other hand, state events are quite interesting, as they also allow to influence the state of the room itself. You could think of them as being building blocks of a key-value store that represents the room state (where "key" is "event type" + optional "state key"). The server has dedicated APIs to read individual state values and also to retrieve the entire state by fetching the most recent events for each "key".
For example, the name of the room is really represented by the m.room.name
event. To read the current name, you can ask for the value by sending a GET request for that specific event type. You can also retrieve the entire state and search for the m.room.name
there. To change the name, you just send a new state event.
This is how we find the timestamps we need for the post — since each event has a timestamp, we can pick the meaningful ones. I've decided that the moment I set a slug for a post is the moment when I consider the post "published" — so the publishing date is the time of the latest canonical alias event that actually sets (not clears) an alias.
Edit: I mentioned above how the first message would be representing the post content. Turns out that getting the first message of a room statelessly is tricky. The endpoint used for retrieving room messages requires specifying a token that indicates where events should be returned from. We don't have one without calling /sync
before. To work around this, I introduced a custom state event named co.hirsz.blog.post_content
that holds the event ID of the post message, e.g. {"event_id": "$something"}
. This makes it easy to fetch the event along with the rest of the state.
Going beyond the stable protocol
So far, everything I described is part of the stable Matrix Client-Server API (r0.6.1). However, to have a functional blog we need more: we still can't edit messages and don't know how to get a list of posts, which is pretty basic functionality. Fortunately, the Matrix protocol is constantly evolving through Matrix Spec Changes (MSCs), some of which are already implemented in servers.
Editing messages is already implemented in Synapse, the reference server implementation, and is specified in MSC2676. Basically, editing works by sending a new message marking that it replaces the old one. The nice thing about the implementation is that you can still reference the event ID of the old message — it will automatically have the new content.
To represent a list of rooms, I've decided to use Spaces — the newest, shiniest feature of Matrix. Spaces are special kinds of rooms — they can contain other rooms. You can probably see how it's useful: since blog posts are rooms, we can have a single parent room that contains all the rooms for posts. Since rooms are linked to spaces using state events, we could just use that. However, I went with the dedicated Spaces Summary API, mostly to see how it works.
Final thoughts
Since it's not exactly fun to write blog posts by sending HTTP requests with cURL (although you could do that!), of course, I had to build the entire stack:
- a simple JS Matrix client to learn how the protocol works (yes I know
matrix-js-sdk
exists) - a JS library on top of the above to expose a blog-centric interface — matrix-blog
- an editor to browse and write blog posts using Markdown — matrix-blog-admin
- the user-facing blog website itself
All in all, this was a fun, if a bit lengthy journey in rediscovering modern front-end development. It took me about a month to get here, but it's finally more or less done. Of course, there's always work to do — for instance, image uploads would be nice — but that can be done in further iterations. Maybe I'll write about them in future posts.
If you want to take a look at the ugly code I've written that implements the above — check out the GitHub repo. For an example of how this works in practice, check out matrix-blog-admin on GitHub, the post editor app I wrote.