Building a Web Version of Your Mastodon Archive with Eleventy

A couple of days ago Fedi.Tips, an account that shares Mastodon tips, asked about how non-technical users could make use of their Mastodon archive. Mastodon makes this fairly easy (see this guide for more information), and spurred by that, I actually started work on a simple(ish) client-side application to support that. (You can see it here: https://tootviewer.netlify.app) This post isn’t about that, but rather, a look at how you can turn your archive into a web site using Eleventy. This is rather rough and ugly, but I figure it may help others. Here’s what I built.

Start with a Fresh Eleventy Site #

To begin, I just created a folder and npm installed Eleventy. I’m using the latest 2.0.1 build as I’m not quite ready to go to the 3.X alpha.

Store the Archive #

I shared the guide above, but to start, you’ll need to request and download your archive. This will be a zip file that contains various JSON files as well as your uploaded media.

My thinking is that I wanted to make it as easy as possible to use and update your Eleventy version of the archive, so with that in mind, I created a folder named _data/mastodon/archive. The parent folder, _data/mastodon, will include custom scripts, but inside archive, you can simply dump the output of the zip.

Expose the Data #

Technically, as soon as I copied crap inside _data, it was available to Eleventy. That’s awesome and one of the many reasons I love Eleventy. While the data from the archive is "workable", I figured it may make sense to do a bit of manipulation of the data to make things a bit more practical.

To be clear, everything that follows is my opinion and could probably be done better, but here’s what I did.

First, I made a file named _data/mastodon/profile.js which serves the purpose of exposing your Mastodon profile info to your templates. Here’s the entire script:

// I do nothing except rename actorlet data = require('./archive/actor.json');module.exports = () => {	return data;}

So, I started this file with the intent of removing stuff from the original JSON that I didn’t think was useful and possibly renaming things here and there and… I just stopped. While there are a few things I think could be renamed, in general, it’s ok as is. I kept this file with the idea that it provides a ‘proxy’ to the archived file and in the future, it could be improved.

For your toots, the Mastodon archive stores this in outbox.json file. I added _data/mastodon/toots.js:

let data = require('./archive/outbox.json');module.exports = () => {	return data.orderedItems.filter(m => m.type === 'Create').reverse();}

This is slightly more complex as it does two things – filtered to the Create type, which is your actual toots, and then sorts then newest first. (That made sense to me.) Again, there’s probably an argument here for renaming/reformatting the data, but I kept it as is for now.

Rendering the Profile #

With this in place, I could then use the data in a Liquid page like so:

<h2>Mastodon Profile</h2>{{ mastodon.profile.name }} ({{ mastodon.profile.preferredUsername }})<br>{{ mastodon.profile.summary }}<h2>Properties</h2><p><b>Joined:</b> {{ mastodon.profile.published | dtFormat }}</p>{% for attachment in mastodon.profile.attachment %}<p>	<b>{{ attachment.name }}: </b> {{ attachment.value }}</p>{% endfor %}

Right away you can see one small oddity which I could see being corrected in profile.js, your join date is recorded as a published property. I really struggled with renaming this but then got over it. Again, feel free to do this in your version! That dtFormat filter is a simple Intl wrapper in my .eleventy.js config file.

Ditto for attachment which are the ‘extra’ bits that get displayed in your Mastodon profile. You can see them here:

Screenshot of my Mastodon profile

With no CSS in play, here’s my profile rendering on my Eleventy site:

Screenshot of my Mastodon profile via Eleventy

That’s the profile, how about your toots?

Rendering the Toots #

I just love the word "toot", how about you? I currently have nearly two thousand of them, so for this, I decided on pagination. My toots.liquid file began with:

---pagination:    data: mastodon.toots    size: 50    alias: toots---

That page size is a bit arbitrary and honestly, feels like a lot on one page, but it was a good starting point. My initial version focused on rendering the date and content of the toot:

<style>div.toot {	border-style: solid;	border-width: thin;	padding: 10px;	margin-bottom: 10px;}</style><h2>Toots</h2>{% for toot in toots %}<div class="toot"><p>	Published: {{ toot.published | dtFormat }}</p><p>{{ toot.object.content }}</p><p><a href="{{ toot.object.url }}" target="_new">Link</a></p></div>{% endfor %}

At the end of the page, I added pagination:

<hr><p>Page: {%- for pageEntry in pagination.pages %}<a href="{{ pagination.hrefs[ forloop.index0 ] }}"{% if page.url == pagination.hrefs[ forloop.index0 ] %} aria-current="page"{% endif %}>{{ forloop.index }}</a></li>{%- endfor %}</p>

While not terribly pretty, here’s how it looks:

Screenshot of my Mastodon Toots

Not shown is the list of pages, which at 50 a pop ended up at thirty-seven unique pages. I don’t think anyone is going to paginate through that, but there ya go.

Supporting Images #

One thing missing from the toot display was embedded attachments, specifically images. In the zip file, these attachments are stored in a folder named media_attachments with multiple levels of numerically named subdirectories. A toot may refer to it in JSON like so:

"attachment": [	{		"type": "Document",		"mediaType": "image/png",		"url": "/media_attachments/files/112/689/247/193/996/228/original/38d560658c00a4e8.png",		"name": "A picture of kittens dressed as lawyers. ",		"blurhash": "ULFY0?s,D%~W~p%Js+^+xpt6tR%LRQaeoes.",		"focalPoint": [			0.0,			0.0		],		"width": 2000,		"height": 2000	}],

Not every attachment is an image, but I turned to Eleventy’s Image plugin for help. It handles everything possible when it comes to working with images. Using a modified version of the example in the docs, I built a new shortcode named mastodon_attachment to support this:

eleventyConfig.addShortcode('mastodon_attachment', async function (src, alt, sizes) {	/*	todo, support other formats	*/	let IMG_FORMATS = ['jpg','gif','png','jpeg'];	let format = src.split('.').pop();	if(IMG_FORMATS.includes(format)) {		// check for valid image 		let mSrc = './_data/mastodon/archive' + src;		let metadata = await Image(mSrc, {			widths: [500],			formats: ['jpeg'],		});		let imageAttributes = {			alt,			sizes,			loading: 'lazy',			decoding: 'async',		};		// You bet we throw an error on a missing alt (alt="" works okay)		return Image.generateHTML(metadata, imageAttributes);	}	// do nothing	console.log('mastodon_attachment sc - unsupported ext', format);	return '';});

Breaking it down, it looks at the src attribute and if it’s an image, uses the Image plugin to create a resized version as well as return an HTML string I can drop right into my template. I went back to my toots.liquid template and added support like so:

{% if toot.object.attachment %}	{% for attachment in toot.object.attachment %}		{% mastodon_attachment attachment.url, attachment.name %}	{% endfor %}{% endif %}

The name value of the attachment ends up being the alt for the image, and currently, I just ignore non-images, but you could certainly do something else, like link to it perhaps for downloading at least. Here’s an example of it in use:

Screenshot of a toot with an image

Show Me the Code! #

Ok, this was all done in about an hour or so, and as I think I said, it’s ugly as sin, but in theory, if you make it prettier then you’re good to go. You can deploy, wait a few months and get a new archive, unzip, and deploy again. Feel free to take this code and run – you can’t make it any uglier. 😉

https://github.com/cfjedimaster/eleventy-demos/tree/master/masto_archive

Raymond Camden

Posted in: JavaScript

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.