[
  {
    "slug": "blogging-take-3",
    "title": "Blogging, take three",
    "excerpt": "Third time is the charm - This time I'm doing things differently, writing about whatever is on my mind rather than sticking to a specific theme.",
    "content": "I'll keep it quick and simple for the very first post on this blog. The idea of blogging has spoken to me a couple of times now. I've tried having a blog about Software engineering, something I do for a living getting some of the ideas out there. But only a few posts in and it already started to feel like work after i was done with work. Then I had a blog on home automation, you know the usual suspects; home assistant, getting a dashboard, trying some ESP8622 stuff. Sadly this kind of stuff gets (to me at least) repetitive quite fast, add another light bulb, update home assistant, again. And within months i felt like i was out of content I was interested in to write about. Third Time's the Charm So, third time is the charm they say, and this time I'm doing things differently, starting of by not using wordpress that alone will already help with keeping this running. But more importantly, I have decided that this time around I'm not going to theme the blog, instead I'm going to write about whatever I have on my mind which I feel like i need to share. In the future I'm going to try and at least put some form of tags on, so if you really feel the need to only find one section of the things i do on here, that should help you out (that is if when you read this that actually works). Either way, I'm looking forward in giving blogging a third proper try.",
    "date": "2021-06-19",
    "tags": [
      "blogging",
      "personal"
    ],
    "author": "Eduward Post",
    "url": "https://www.eduwardpost.nl/posts/blogging-take-3/"
  },
  {
    "slug": "federating-my-blog-part-1",
    "title": "Federating my blog (part 1)",
    "excerpt": "Implementing the ActivityPub protocol on Umbraco to put my blog on the Fediverse, including WebFinger, actors, and the inbox system.",
    "content": "import ResponsiveImage from '@/components/ui/ResponsiveImage.astro'; With the release of Threads (though not in Europe) ActivityPub is a hot topic once again while Elon Musk seems to continue on world record pace for the any% speedrun how to kill twitter. If Threads indeed federates as they said they would this might just be the final nail in the coffin of twitter. Last month alone there was an influx of over 4,5 million users (for a total of just over 10) (source: fedidb.org) You can definitely tell the Fediverse is working on it’s second run, so why not join in? Of course I’m already on mastodon (link) but I also own a blog (if you can call this a blog 2 articles in 3 years, so much for that proper try). I’ve build this blog in Umbraco, mostly as a playground for me to at the time learn the ropes of Umbraco, test custom code and plugins etc. so lets test the next new thing, putting my blog on the Fediverse! Here's how I envisioned this would go down: Read up on ActivityPub Prepare my Umbraco setup (document types and such) Implement ActivityPub Profit? What is ActivityPub? Though I think most of the people reading this are aware of what ActivityPub is, a short rundown is never amiss. ActivityPub is an open networking protocol (as part of the W3C) that can be used to communicate between social decentralized social networking platforms (like Mastodon). Though I can also imagine ActivityPub (if it is here to stay) might catch on for other uses as communication protocol between different parties. It defines both a client to server API as well as an federated server-to-server API. In the this case I’ll be implementing the server-to-server part of ActivityPub to put my blog on the Fediverse (the name of the “federated” universe). Preparing Umbraco My Umbraco setup is anything but complicated, it’s a simple home page, with 7-page types (if you include the sitemap, error page and random content page type that I’m not using) of which the one that is the most important for this is of course the Article page type you’re looking at right now and even that one is straightforward. The idea I had was that you wanted to “post” blog items on the Fediverse as the user you’re logged into in in Umbraco. This way if you have a blog with more then one person writing on it you could follow that specific person on your Federated social media platform and get an update every time that person posts. (At some point I want to try and make a proper Umbraco package of this so it can be easily implemented by whomever wants to, like someone who actually fills their blog). Though later on I might also add an site white actor that would post all blog items, not just from that user. Anyway, since I’m making the Umbraco user the key for the actor, I need to link the blog posts to the specific user. I had a textbox field on my page that allowed me to write my name down as the “author” of the blog post but in order to link the post to the User I changed this to an “User Picker” field and changed the views accordingly to display the name form it. Creating the actor Creating the inbox Creating the inbox actions Mastodon (why checking things is important) Weird things about the spec (@context is both and array and a string) Next steps Getting Found on the Fediverse Having prepared Umbraco for the Fediverse the next step is to get found on the Fediverse. For other servers to find my blog (or more precisely users on other servers) there are two things that need to be done, first I need to create an ActivityPub actor and publish it on an URL. After that I need to implement the WebFinger Protocol so that servers can query my server on where to find said actor (and if it even exists). The WebFinger Protocol The WebFinger protocol is used to query servers on a known location about information on that server, in this case about an actor. The protocol is not only used for Fediverse platforms but for example also for the OpenID Connect standard. The query that a Fediverse platform like Mastodon (I’ll use that as the example, but it should work for the majority of ActivityPub platforms) use the protocol to request the information about an ActivityPub actor. In my case that actor would be eduwardpost@www.eduwardpost.nl like with a user on a different Mastodon server then I’m on it contains both the username (of the local server) as well as the server information, separated by the @. The request path that Mastodon does (in the case of my blog) is then: Where the first part (the path) is a fixed path on every server implementing it (like with the other .well-known paths) and then in the query string information about what you want to know, in this case a resource, specifically the account (acct) eduwardpost@www.eduwardpost.nl My server then responds with the JSON to the right. The document is pretty simple, it describes the subject of the document (the account) and links to where to find more information, in this case the “self” of type “application/activity+json” and the actual link. The content type was new to me though as it is part of the ActivityPub specifications. The technical implementation of this in Umbraco is about as simple as the document, it’s a POCO object that is being return by an custom controller that extends the UmbracoApiController. Then in the Action I look at the query string, check if it contains resource and if its value starts with acct: if so, I string split it twice, first on : then on the @ which gives me the username the requester is interested in which I then use to find the IUser object with the IUserService from Umbraco. I’ve used the IUser object at all the parts where I need to know something about the actor as this would make it possible to automatically create all the documents for the different users in the Umbraco implementation. The ActivityPub Actor So now that Mastodon is able to check if the user exists on my server, and knows where to find it, I need to prepare the next part; the Actor. This one not as simple as the WebFinger file, and is the first ActivityPub part of the implementation. In short it holds a list of locations (url’s) of specific ActivityPub parts (more on those later). Some basic information like username, preferred name, icon and some default fields that any ActivityPub object has like an id, object type and a context property as defined in the “JSON-LD” specification. All straight forward information. Except for one, the public key field, though this field is not specified in the ActivityPub protocol it is part of an other W3C protocol that is used for security. In this case it allows severs to check if a message received on their inbox (more on that later to) is indeed send by the actor that it says it is. And it uses normal RSA encryption to do so. Therefor we need a way for servers to get our public key, the security protocol specifies it to be in the publicKey property and in PEM format. In the end it looks like the JSON on the right here, note that the public key has it’s new lines replaced by \\n characters. Though most information is about as straightforward as the webfinger document and thus easy to create a POCO for with a custom controller, which is how I implemented this in Umbraco. The key part was a bit more involved, to prevent any security issues I didn’t want to use any of the SSL keys that where already in use (like for serving HTTPS traffic). And since I wanted it to be possible for all Umbraco users to be on the Fediverse I needed a way to generate the keys in the application (since you can add users later to). The followers, inbox and outbox links I’ll come back to later So on first request if there’s no key pair I generate a new pair using the .net RSA implementation (though this can also be changed to openssl later on, again if it becomes a package). This I then store on the server (this to should / could be changed later to something like say… an Azure key vault, but since I don’t use the key elsewhere I’m not that concerned right now). From the controller perspective, like with the WebFinger controller it’s a custom Api controller that uses the path part to get the IUser and creates the Actor document with that. Am I Federated? From the controller perspective, like with the WebFinger controller it’s a custom Api controller that uses the path part to get the IUser and creates the Actor document with that. So far so good, at this point I deployed it to production and went to Mastodon, entered eduwardpost@www.eduwardpost.nl in the search field, waited for the results to load and… there it was! Mastodon found a user known as eduwardpost on the server www.eduwardpost.nl, it works my server is now on the Fediverse! Of course the immediate next step is to press that follow button, with high (or vain) hopes I press the button and to my shock it says “following” seemed easy, to easy… after a quick reload of the page that then changed to “cancel follow request” turns out, you need to accept a follow from someone in the Fediverse. And thus it’s on to the next part, the inbox. You've Got Mail! In Your Inbox... So, Mastodon can find me, I press follow and that tells the mastodon server of the instance I’m on to send a message to my Umbraco server and deliver it to my actor’s inbox. So yes, I am now federated! Mastodon looks at the actor file for an location of the inbox (as defined in the ActivityPub protocol) to deliver said message. The ActivityPub inbox is “just” and HTTP endpoint that listens to a POST request with in it’s body an ActivityPub Activity. In this activity there’s a bit of information mostly an id and type (like any ActivityPub object) the actor sending (publishing) said activity and an object that describes the activity, in this case the actor that is going to be followed. In JSON that looks like this: As with the actor this to is \"just\" and Simple HTTP Post action on the custom controller I made, in which I store the activity in a database (for later things like placing reactions beneath the blog post that have been made on Mastodon for example). So, I process the request return a nice 200 OK result back to mastodon, repress the follow button… same thing happens, it changes to cancel request, even though I stored it in the database, turns out you need to create an accept message and hand that back to the requestor. This is where things get complicated… Accepting the Follow At first I thought this was pretty simple, I checked the documentation did some quick googling, browsed some Github pages on people’s ActivityPub implementations and quickly found how the accept message should be modeled, it’s an Activity message just like the Follow request which I already have a POCO for. Only differences are that the type now is Accept instead of Follow, the actor is now my instead of the Mastodon actor and the object contains the entire Activity as an JSON object. This didn’t take to long to code, reference the request object in the response object, set the type and actor, put it in the response body and go! Trying to follow again (though by now I also figured out there’s an Undo action that is being send when you cancel the follow, so after implementing that which doesn’t do much but “undo’s” the database record that the follow created). Same thing still. This took me longer then I like to admit, turns out I didn’t read well enough (theme for the rest of this part) instead of returning it in the body of the request, you need to send the accept activity message to the inbox of the actor that send you the activity with the follow request… I can see know how the first rush of the Fediverse caught some Mastodon admins off guard with all that additional network traffic. Sending Messages to Mastodon Ok so, we need to send a message to the requester’s inbox. Most of this is pretty straight forward, get an HttpClient, put in the address serialize the body and send, except remember that publicKey on the actor? This message needs to be signed, this to is not rocket science and with some quick googling, githubing, stackoverflowing, etc. I figured out Mastodon (and many others) uses part of an non finalized specification for HTTP header signatures. On top of that it requires a specific message to be singed specifically (to the left here) Including the newline (\\n) characters, all not hard information to gather but sure made the idea of “lets quickly build this in one evening” go out of the window. So… that should be it right?! Wrong, sending this nicely gave a 202 accepted response back, but didn’t change the button on Mastodon, so what’s up with that. First it turns out Mastodon practices in silent fails (or at least the instance I’m on) so it said it accepted it, but it didn’t. So much for evening two, which mostly was spend in frustration and google actions. Trying and erroring many different things (there’s quite some old documentation about ActivityPub/Mastodon on the web to that no longer works which I stumbled on and tried) For day three I went through it all with a fine comb again and I found it! Turns out I again was not paying attention to a specific detail, remember that context field that is not part of the specification of ActivityPub? If you look at the JSON’s on the page you’ll see that it’s property key is “@context”. Which I forgot to nicely defined in my POCO with a JsonPropertyNameAttribute. So it did not get serialized in the message I send with that key but with “context” instead, Mastodon didn’t like that and silently fails my request. Having found that and fixed it, I ran into the next weird thing. All of the sudden I was no longer able to parse the message on my Http Action controller now that it is nicely picked up but the JSON converter. All the documentation I’d seen so far that had this property defined it as an array of strings in the JSON, and for the actor it even had two values in it. Though most ActivityPub messages only have one. Turns out if it only has one Mastodon returns it with a string instead of an array with one string… Turns out that’s allowed in the spec, the @context field can be about anything, string, array object it’s all fair game for this field. One quick JsonConverter further the messages where parsing again, and on top of that my Accept ActivityPub Activity message finally got accepted and I’m not following myself! (From Mastodon to my Umbraco blog that is) So Now What? As the title already indicated this is Part 1 of a multi part (at least two) series. Because the whole idea of following someone is that you get to see their posts on your timeline, which in this case would be the blog posts of course! Which is where the outbox part of the actor comes into play. Also one of the other next steps is ~~make a proper code view on this blog (and update this page with them) because this looks appalling.~~ edit: this has since been done. If you do read this before I post part 2 you can follow my blog on your Fediverse instance (hopefully, as I so far only tested with Mastodon) by following eduwardpost@www.eduwardpost.nl and hopefully you get a post on your timeline when it shows up. And if you’re scared I’d spam your timeline, there’s no need with 2 articles in 3 years I wouldn’t expect much",
    "date": "2023-07-19",
    "tags": [
      "activitypub",
      "fediverse",
      "umbraco",
      "mastodon",
      "web-development"
    ],
    "author": "Eduward Post",
    "url": "https://www.eduwardpost.nl/posts/federating-my-blog-part-1/"
  },
  {
    "slug": "federating-my-blog-part-2",
    "title": "Federating my blog (part 2)",
    "excerpt": "Posting to the Fediverse from Umbraco - Creating the outbox, sending updates to followers, and improving the link sharing experience.",
    "content": "It’s been a week and a bit since part one of this story, since then a few things happened. Threads’s user base has more than halved (from it’s 49 million peak to under 12 million), I got a new keyboard but more importantly part two of this project is finished. Part one (part one) was a lot more involved as I had to dive head first into the activitypub protocol as a whole, struggle with Mastodon’s decision to produce silence fails and figure out how to write a blog post for the first time in a year. This time around my goal is to get the blog post to show up on mastodon on the post history and in the feeds of the followers of my blog. And if things go alright (which they did if you’re reading this) then you should’ve seen this article as a post on your Mastodon feed, if you follow my blog that is. (or me as I’ll probably boost it) The Plan for Part 2 There's a few steps to be done this time: Figure out how to get posts to show up on Mastodon if you search for the blog Figure out if/how to update your followers that a post has been made Try and improve the link sharing experience for the blog Creating the Outbox Creating the outbox The first thing I wanted to get going was to get the post history to show up on Mastodon. This seemed to be reasonable straightforward. As you might’ve seen in part one the actor object has a link defined called “outbox”. Where the inbox is for receiving messages, the outbox is for sending messages out, makes sense. The activity pub documentation is also quite simple. The outbox contains a list of messages for the called to see. If you call it unauthenticated you should see whatever is publicly available, if you call it with authorization you show the public and private activities, for whomever authorized. The outbox returns an object known as “OrderedCollection” (see below) most collections in ActivityPub can be either a normal collection or an ordered collection, but the outbox specifically requires an ordered collection, where the order is publication date. I already made the code for this in part 1, as I tried this as one of the debug steps to make it possible to follow the blog on Mastodon thinking that it was a required property for the social network. Which would make sense to some degree but turned out to not be the issue. Though even though following worked after figuring out what the issue was, the posts never showed up. I did notice that there was some sort of caching going on at the Mastodon server when I tried to change the icon of the blog. I changed the actor document, published the code and it took about a day for it to reflect on Mastodon when searching for the blog. Which was going to make this part a lot more annoying, as you can’t be sure that the change you make is not fixing the issue, or if the cache just needs to invalidate. Diving Further into Mastodon Still trying to get the posts to show up on Mastodon I wanted to do a control test, in a controlled environment. And since Mastodon is decentralized it would be possible to create this controlled environment. I got both a hosted environment as well as spun up an environment from the source code. The hosted instance was simple enough to get going, swipe the credit card enter some data and your mastodon instance is up and running. Getting an instance up and running from code is a lot more involved, though if you follow the steps one by one on the Mastodon site in the end it does boot and work (don’t forget to compile the CSS and JS… things might look really odd otherwise). This gave me a fully federated controlled environment where I could test and get crazy with accounts without spamming people’s timelines. And on top of that on the source build one I can see all the debug logging. After some trial and error, I figured out that posts that are made before you follow someone on the other server never show up. Instead, they automatically appear once you post something while someone on the other server is following you. Meaning that step one of my three step plan automatically gets completed when I create the second step. Hitting That Bell During this project I’ve gathered quite a few resources on ActivityPub and Mastodon, this time things went smoothly for once, I already had the right Activity objects as it’s the same as in the outbox. Already had the signing code ready and tested from part 1. Quick abstraction layer later and I can send an post to an inbox of an actor. On top of that Umbraco has a few easy hooks into the CMS, in this case I used the “ContentPublishedNotification” which you can register as an event handler and gets called whenever a user publishes a piece of content in the CMS. Things are very simple from here, loop over your followers (that I saved from step 1) send them an activity on their inbox nicely singed by your private key and like magic it shows up on their feed and in the post history if you search for the blog on that server. The only thing (that I found out the hard way on my test Mastodon instance) that I had to add after that is keeping track of which posts I already send the update to the inbox. Since the notification from Umbraco gets send for every time you publish, also if it’s just fixing a typo. Styling It Up Now that the blog is going to be shared (at least once by the blog itself) the share link should at least look somewhat decent. I never really looked into this, but it’s pretty straightforward add some meta tags (that I may or may not have stolen from an random post I saw) and fill them with the content from the article and all is set. Next Steps? So now that I’ve implemented ActivityPub, or at least the basics of it what’s next? My first things would be to clean up the code, write some proper tests. Then I think this would be a great bit of code to make it into an real Umbraco plug in which also is something I’d want to give a shot. But if I am going to publish this I need to add some more features to, like configuration settings, being able to defederate from specific actors and make it possible to put it in “single user mode” instead of an actor per Umbraco User. On top of that I would like to maybe add a commenting system on the site that works through activity pub, comments on the post in the Fediverse then would show up on the site as well. For now, this is good enough, so no promises on when the next installment arrives.",
    "date": "2023-07-31",
    "tags": [
      "activitypub",
      "fediverse",
      "umbraco",
      "mastodon",
      "web-development"
    ],
    "author": "Eduward Post",
    "url": "https://www.eduwardpost.nl/posts/federating-my-blog-part-2/"
  },
  {
    "slug": "going-static",
    "title": "Going static, kinda",
    "excerpt": "Making my blog static - and hosting the server side components in cloudflare.",
    "content": "import NewTabLink from '../../components/content/NewTabLink.astro'; import MermaidDiagrams from '../../components/content/MermaidDiagrams.astro'; There are so few posts on this blog that I've decided to make it static, just plain old HTML on some random web server. Well, not really, but with the number of posts I could do that and it would be workable. I have, however, moved this site away from Umbraco and to a static site generator setup. Not because Umbraco is bad, quite the opposite if I require a CMS, I would still go for Umbraco. However, for this blog I don't need a full-blown CMS, instead, I'm going to keep it simple. Picking a framework To choose the framework I wanted to use, I asked my friend ChatGPT to give me a list of different static site frameworks, their pros and cons, with a few constraints. Mostly, it needs to be able to use TypeScript, have components and, most importantly, run on Cloudflare Pages. It came back with this table | Framework | TypeScript | Components | Static output | Cloudflare Pages | Best use | | ------------------------- | --------------------: | -----------------------: | --------------------------: | ---------------: | --------------------------------------------------- | | Astro | Excellent | Astro + React/Vue/Svelte | Excellent | Excellent | Blogs, portfolios, community and business sites | | SvelteKit | Excellent | Svelte | Excellent with adapter | Very good | Interactive sites and lightweight applications | | Nuxt | Excellent | Vue | Excellent | Very good | Vue-based websites and applications | | React + Vite | Excellent | React | Client-side SPA | Excellent | Dashboards and browser applications | | Next.js static export | Excellent | React | Supported with restrictions | Usable | Existing React/Next projects | | Eleventy | Possible | Template-oriented | Excellent | Excellent | Simple sites, but weaker component ergonomics | | Hugo | No native TypeScript. | Go templates | Excellent | Excellent | Huge content sites, but not a TS component workflow | Astro jumped out of that table mostly because it aligned with my use case the most and seems to be quite flexible if I want to add more complex things later on. It was then up to Copilot to migrate my blog into an Astro-based website, migrate the content to Markdown files and implement a few styling updates I've had on my mind, like adding some colour and making a few things more... stylish. This was the easy part, as I did not want to lose the functionality I already had on the site, which is mostly based on server-side things. So the next step was how to move server-side components to Cloudflare, and also, why Cloudflare. Why Cloudflare While I think the move from a CMS to a static site generator might be more understandable, the move from server to serverless might need some more explanation. It's mostly based on two wants: no longer having to manage anything for this site and wanting to learn about using Cloudflare's serverless stack. While not wanting to manage anything is definitely a win for this site, it's also the least important one, as I run a Kubernetes cluster at home (maybe more on that later this year, if I make another post). The Cloudflare stack, however, has been on my mind for a while, both because they offer it for free with limited use and because I've been hearing good things about it and want to see what kind of solutions can be made with it. And since my site had a few server-side parts to it, mostly the search and the ActivityPub integration, I wanted to move those to this Cloudflare stack. Serverless server-side There are two parts that were server-side on my blog: search and ActivityPub. I moved both over to Cloudflare with different implementations, and both migrations were done with assistance from Copilot. Search The search field on the site is definitely the simpler one, and thus this was the first one I migrated. While it is a static site, there's one part that still gets dynamically rendered on the client side of the application: search results. The goal was not to have any server-side components that render content, so instead I created a TypeScript-based implementation in Astro that could fetch JSON from an endpoint and use that to render items on a page. And since there's no backend that can dynamically generate content, I could make the indexing of the site for searching quite static as well. Thankfully, Astro has the ability to create \"API\" endpoints; in this case, it generates the responses for these endpoints at compile time and serves them statically. I use this both for the RSS feed as well as the search index, which is just a JSON object served on this site. So, with the search index generated at compile time and the search results rendered on the client side, the search functionality only requires one last core component: actually searching. I've solved this with a simple TypeScript-based search implementation that can search through the JSON object and return results. This script is running in a Cloudflare Worker, which is a serverless function that can be called from the web. While it would be possible to have the search functionality run on the client side, I wanted to use this as a test case for Cloudflare Workers and thus have the search functionality run on the serverless stack. Not only was this a good, simple use case to test Cloudflare Workers, it was also a good, simple entry point to help me build the CI pipeline in GitHub, both deploying the site to Pages for main and preview builds and deploying the Cloudflare Worker to Cloudflare. ActivityPub The ActivityPub implementation was a bit more complex, as it required a few more things to be implemented. I started by letting Copilot reverse-engineer the specs based on my Cimplementation and blog posts. Since I still find the documentation a bit lacking, I wanted to be sure that all the discovery work I put into my previous implementation was not lost in this migration. The next step was to see how I could fit all these components into the Cloudflare stack, especially since it's not a full server and there are some different limitations and options to work with. In the end, I came to the conclusion to use a variety of Cloudflare services to implement the ActivityPub protocol. I created a second Worker that would handle all ActivityPub-related compute. On top of that, I chose to create a D1 database to store the ActivityPub data, a KV store to store the public and private keys for signing and verifying requests, and a queue for handling async activities. The queue is used to handle the delivery of activities to other actors, as this can be a time-consuming process and I don't want to block the main Worker from handling requests. This also makes it possible to break the processing up into smaller pieces, which is very helpful for staying within the 10 ms CPU time that comes with the free Cloudflare Workers plan. AP Q --> AP AP --> KV1 AP --> D1 AP --> QUEUE} /> Let's run through it component by component, starting with the D1 database. This is used to store all the ActivityPub data, including actors, activities, objects and more. The D1 database is a relational database that can be queried with SQL, which makes it easy to store and retrieve data. I need to store some data because, to post a post to feeds, we need to know which inboxes to send the activity to, and to know which inboxes to send to, I need to know which actors are following us, and to know which actors are following us, I need to store that data somewhere. I also need to store which posts I have already \"posted\" and which posts we need to post, because I don't want to post the same post multiple times. And since the site is static, I can't do it with an on-publish hook like I did in the Cimplementation. Instead, I created a sync process that runs at deploy time on the Cloudflare Worker, checks for new posts and adds them to the D1 database. This is done by fetching the RSS feed and checking for new posts. And when a new post is stored, the process of sending it to the followers is also started. This is done by adding the post to the queue, which will be processed by the Worker and send the post to the followers. That mostly also explains the queue. It's used to handle the delivery of activities to other actors, as this can be a time-consuming process and I don't want to block the main Worker from handling requests. But besides handling new posts, it is also used to handle all other incoming activities. Finally, the KV store is used to store the public and private keys for signing and verifying requests. We don't really have many keys to store (since we only have one actor), but it is a good place to store them, as it is a key-value store and we can easily retrieve the keys when we need them. This is mostly just because I wanted to use KV for my own learning purposes. Implementing ActivityPub, again After deciding on the architecture of the ActivityPub implementation, I started to implement the Worker. This was done with the help of Copilot and a lot of reading of the ActivityPub specs. I mostly let Copilot generate the code based on my previous implementation and the specs, and made some changes to keep it within the coding standards I wanted. On top of that, I rewrote the test set I have in my Cimplementation into a TypeScript-based test set that can be run with Vitest. This was a great help in testing the implementation and making sure it was working as expected. And while you do have to refollow this blog to get the new posts, as I could not be bothered to write an actual database migration and wanted to change the handle of this blog, it does seem to work, except that, for whatever reason, the logo does not seem to load on Mastodon.",
    "date": "2026-06-14",
    "tags": [
      "activitypub",
      "fediverse",
      "cloudflare",
      "astro",
      "web-development"
    ],
    "author": "Eduward Post",
    "url": "https://www.eduwardpost.nl/posts/going-static/"
  }
]