How I Stopped Worrying and Migrated This Website to a Decentralized Net
Aug 18, 2024
I want to create a blog… But before that, I need to over-engineer my Mojo Dojo Casa Blogging Platform from scratch!
Background
At the time of writing this post, I am located in Russia. And, if you’ve heard recent news… More and more tech turns into a pumpkin (but this is another long story). The idea for this post came up when I was tinkering with the Radicle — an open-source, peer-to-peer code collaboration stack built on Git — looking for a better alternative to GitHub, which is also in a hurry to turn into a pumpkin.
Radicle extends the Git interface, allowing you to push your repo to the decentralized network of peers.
You do rad init inside your repository, and it receives a unique repository ID (RID), which further
serves as the public identifier (unlike <username>/<repo-name> in centralized alternatives).
Then you push to the magical rad remote
git push rad <your-branch>
And voila, your repository starts to spread across the network!
Seeing fancy frames and colored symbols in the terminal I became determined to migrate all the fruits of my labor from the GitHub to the Radicle.
Migrating this website
This blog (with zero posts) was always powered by Hugo and hosted on the GitHub Pages. Radicle doesn’t currently offer any CI/CD options (but is developing some) and doesn’t offer any «Pages» service. I could use Cloudflare Pages or something similar, but
- Who knows, Cloudflare can turn into a pumpkin as well
¯\_( ͡° ͜ʖ ͡°)_/¯ - Without CI/CD that would require installing a special CLI and integrating it into my local workflow
- I just wanted to dig deeper and discover some decentralized solutions
Disclaimer: my final setup isn’t fully decentralized (and uses Cloudflare, heh), but close enough.
My final goals were:
- The website should be hosted under my domain
shishqa.xyz - I should be able to release content without
ssh-ing somewhere, using complex workflows, or opening dashboards on third-party websites. Preferably, just using Git and Hugo - No money :)
- The website should be generally available from any location with OK-ish load times
The solution came surprisingly fast.
Hosting website directly on the Radicle network
In the cycle of moving repositories to the network I accidentally pushed a Hugo output directory with rendered HTML files. I’ve noticed this while browsing the repo on one of the nodes:
You know what happened next. I clicked raw view and to my surprise the browser showed a rendered HTML page instead of raw source code (like GitHub does). Some links were broken, some styles and fonts didn’t render, I’ll cover some tweaks I’ve made later. But the first thought was Wow, is this a decentralized ungoverned Pages??? or is it?
And I can confirm that at least by referencing a related discussion in the Radicle community. But there are some difficulties:
Node serving capabilities
The node itself doesn’t do much. In my previous Pages setups, I had links like shishqa.xyz/whoami. But if I pass the whoami path
to the node it will respond with an error, because there is no whoami file in the repo, but only a whoami.html.
This problem was solved quite easily by enabling the following options in the Hugo configuration:
relativeURLs = trueallowed me to serve the content under the prefix/raw/<rid>/<commit_hash>/public/.uglyURLs = truesolved the problem with.htmlsuffix.
URL
The URL I was redirected to has the following structure:
https:// <seed_url> /raw/ <your_RID> / <commit_hash> / <path_inside_repo>.
There are some problems with it:
- When I update content in the main branch I want it to be shown to readers. But, if you know git, you know that a new commit means a new commit hash. So, the URL changes each time I update something, and users are not automatically redirected from the previous commit hash to the next.
Btw, this is a killer feature itself, because I can see my website on any commit without the need to deploy any environments. For example, I work on this post in a branch and can push a commit to the branch and open it in the browser to preview it on the radicle node.
If the <commit_hash> could be replaced with for example <branch_tag> (e.g. main), the URL I want you to open won’t change with push to the main branch. The same discussion gives hope that it will be implemented someday in the Radicle nodes. But there comes the next problem.
Edit: After publishing, cloudhead@ suggested a better solution in this comment. One can simply use
:rid/head/:pathto get the latest contents!
- OK,
<your_RID>,<path_inside_repo>, and<branch_tag>won’t change in time. But what to do with seeds? They are hosted by different organizations and enthusiasts (I love this part about the Internet so much), but how to implement the discovery? It is obvious that each seed in the network is not so reliable and doesn’t have any SLA. And I don’t want to list 10 mirrors everywhere, I want to use my domain name.
I’ve googled a lot and had a lengthy chat with my GPT, and figured out that a) generally, DNS providers don’t allow you to serve something like ash.radicle.garden / raw / rad:z4NUTDwCFyoExPEHanFqvSYKu2Wsk / main / raw / public / XXX under my.domain / XXX (this is called the iframe redirect), Nginx can do that, but b) there are no free Nginx-as-a-Service solutions. I decided to stick with Cloudflare’s free offerings for now.
Configuring a Cloudflare Worker
Cloudflare Workers is the platform, which allows running serverless code for each request. Free-tier is not so generous, but is enough for now (I have less than 10 readers at the moment).
I’ve set up a website and configured Cloudflare nameservers so that I own the DNS record in another registar, but configured DNS in Cloudflare. This also gave me free analytics and some level of protection (but of course I control myself and don’t get used to it, this is a platform that can turn into a pumpkin!)
Then I set up a worker, that triggers each time the user requests a URL shishqa.xyz/XXX. The worker has the following parts:
Seed selection. For now, I decided to stick with a simple solution: select a seed manually. By default, I offer
ash.radicle.garden, but it can be changed by going toshishqa.xyz/at/<seed>/XXX. In the worker, I parse it the following way:const url = new URL(request.url); const urlRegex = /https:\/\/shishqa\.xyz(?:\/at\/([^\/]+))?\/?(.*)/; const match = url.toString().match(urlRegex); if (!match) { return new Response(`Error: bad url`, { status: 400, }); } const seed = match[1] || 'ash.radicle.garden'; var suffix = match[2] || '';After selecting a seed, I need to get the latest available commit. For this, I use a Radicle API call:
Edit: this step can be omitted now, simply use
headinstead of commit hash in url.const repoInfo = await fetch(`https://${seed}/api/v1/projects/${rid}?v=4.0.0`); if (!repoInfo.ok) { return FallbackMirrors(seed, suffix); } const headRevision = (await repoInfo.json())['head'];Here
headRevisionis the latest revision of the default branch. TheFallbackMirrorsfunction returns a small HTML page with the list of other mirrors you can try (go to https://shishqa.xyz/at/poop/log/000-moving-away-from-github.html)After these steps, I can fetch the original page from the seed node:
const originUrl = `https://${seed}/raw/${rid}/${headRevision}/public/${suffix}`; const originPage = await fetch(originUrl); if (!originPage.ok) return FallbackMirrors(seed, suffix); return new Response(originPage.body, originPage);Frankly speaking, there is one more step before querying. With the setup above you wouldn’t be able to query just
shishqa.xyz, because of the reason I mentioned: there is no such file. So I need to modify the suffix a bit:const extensionPattern = /.*\.[^\/]*$/; if (suffix.length == 0) { suffix += 'index.html'; } else if (suffix.endsWith('/')) { suffix = suffix.slice(0, -1) + '.html'; } else if (!extensionPattern.test(suffix)) { suffix += '.html'; }Now url
shishqa.xyzwill point topublic/index.html,shishqa.xyz/abcwill point topublic/abc.htmlandshishqa.xyz/abc/will point topublic/abc.htmlas well.
That’s all! You can find the full worker code here. And the repo with the website is here.
Final thoughts
This setup isn’t perfect. I still use Cloudflare Workers with a small quota, but the overall setup is completely free, except I need to pay for my domain. It is always possible to fall back from the domain URL to a Radicle node URL. When referencing files by a branch tag will be implemented, this will be generally usable on Radicle without Cloudflare.
Another caveat is that there is still a small number of seeds in the Radicle network and no seeds in Russia (I’ll probably try to set up one). This means that the network may become unavailable someday. But this will be another story.
What a day to be alive! Thanks for reading ☀️!