Update from the future: For a Tailwind CSS-styled version of the Mastodon-related code herein, see this follow-up post.
In the process of my experimentation with various static site generators (SSGs), I’ve created Eleventy versions of my Hugo shortcodes for doing fully static embeds of tweets and their Mastodon counterpart, toots. I offer them here in somewhat edited form, with their original repo-based versions as noted.
Of course, be sure to enable each of these in your Eleventy config file through the usual procedure. Feel free to rename them if you wish; their respective names are just what I call them.
And, yes, this may finally be the end of my posts on this subject, half a year after I started down this path. Feel free to rejoice. I’ll be right there with ya.
Update, 2022-08-30: Actually, I decided I should do at least one more such post.
Static tweets
Deprecation notice, 2022-11-06
Due to changes in the status and/or availability of one or more Twitter APIs, perhaps due to the many corporate changes at Twitter itself following its purchase by Elon Musk, I have deprecated several posts, or sections thereof, concerning the fully static embedding of tweets within one’s website. However, if you still wish to see the final pre-deprecation form of this post, it remains accessible in the site’s GitHub repo.
Static toots
Using the stoot.js
shortcode in Markdown brings up the following:
I was asked to do a video interview, but in the end they couldn't send a crew to where I live. I know the interview would be good for Mastodon, but I am so relieved I don't have to do it 😅
Invoke the shortcode in Markdown as in this example, where the first parameter represents the toot’s Mastodon instance and the second represents the toot’s numeric ID:
{% stoot "mastodon.social", "108241788606585248" %}
Note that it assumes you have the eleventy-fetch
, luxon
, and md5
packages installed in the project.
stoot.js
const EleventyFetch = require("@11ty/eleventy-fetch")
const md5 = require('md5')
const { DateTime } = require("luxon")
module.exports = async (instance, id) => {
let stringToRet = ``
let tootLink, handleInst, mediaMD5, urlToGet, mediaStuff, videoStuff, gifvStuff, cardStuff, pollStuff = ''
let imageCount, votesCount = 0
urlToGet = `https://` + instance + `/api/v1/statuses/` + id
async function GetToot(tootURL) {
const response = await EleventyFetch(tootURL, {
duration: "2w",
type: "json"
});
return response
}
// Regarding the settings above,
// consult the eleventy-fetch documentation
// at https://www.11ty.dev/docs/plugins/fetch/
let Json = await GetToot(urlToGet);
if (Json.account) {
tootLink = `https://` + instance + `@` + Json.account.acct + `/status/` + id
handleInst = `@` + Json.account.acct + `@` + instance
}
if (Json.media_attachments.length !== 0) {
mediaMD5 = md5(Json.media_attachments[0].url)
Json.media_attachments.forEach((type) => {
if (Json.media_attachments[0].type == "image") {
imageCount = ++imageCount;
}
})
Json.media_attachments.forEach((type, meta) => {
if (Json.media_attachments[0].type == "image") {
mediaStuff = ``;
mediaStuff = mediaStuff + `<div class="tweet-img-grid-${imageCount}"><style>.img-${mediaMD5} {aspect-ratio: ${Json.media_attachments[0].meta.original.width} / ${Json.media_attachments[0].meta.original.height}}</style>`;
mediaStuff = mediaStuff + `<img src="${Json.media_attachments[0].url}" alt="Image ${Json.media_attachments[0].id} from toot ${id} on ${instance}" class="tweet-media-img img-${mediaMD5}`;
if (Json.sensitive) {
mediaStuff = mediaStuff + ` tweet-sens-blur`;
}
mediaStuff = mediaStuff + `" loading="lazy"`;
if (Json.sensitive) {
mediaStuff = mediaStuff + ` onclick="this.classList.toggle('tweet-sens-blur-no')"`;
}
mediaStuff = mediaStuff + `/>`;
if (Json.sensitive) {
mediaStuff = mediaStuff + `<div class="blur-text">Sensitive content<br />(flagged at origin)</div>`;
}
mediaStuff = mediaStuff + `</div>`;
}
if (Json.media_attachments[0].type == "video") {
videoStuff = ``;
videoStuff = videoStuff + `<style>.img-${mediaMD5} {aspect-ratio: ${Json.media_attachments[0].meta.original.width} / ${Json.media_attachments[0].meta.original.height}}</style>`;
videoStuff = videoStuff + `<div class="ctr tweet-video-wrapper"><video muted playsinline controls class="ctr tweet-media-img img-${mediaMD5}`;
if (Json.sensitive) {
videoStuff = videoStuff + ` tweet-sens-blur`;
}
videoStuff = videoStuff + `"`;
if (Json.sensitive) {
videoStuff = videoStuff + ` onclick="this.classList.toggle('tweet-sens-blur-no')"`;
}
videoStuff = videoStuff + `><source src="${Json.media_attachments[0].url}"><p class="legal ctr">(Your browser doesn’t support the <code>video</code> tag.)</p></video>`;
if (Json.sensitive) {
videoStuff = videoStuff + `<div class="blur-text">Sensitive content<br />(flagged at origin)</div>`;
}
videoStuff = videoStuff + `</div>`
}
if (Json.media_attachments[0].type == "gifv") {
gifvStuff = ``;
gifvStuff = gifvStuff + `<style>.img-${mediaMD5} {aspect-ratio: ${Json.media_attachments[0].meta.original.width} / ${Json.media_attachments[0].meta.original.height}}</style>`;
gifvStuff = gifvStuff + `<div class="ctr tweet-video-wrapper"><video loop autoplay muted playsinline controls controlslist="nofullscreen" class="ctr tweet-media-img img-${mediaMD5}`;
if (Json.sensitive) {
gifvStuff = gifvStuff + ` tweet-sens-blur`;
}
gifvStuff = gifvStuff + `"`;
if (Json.sensitive) {
gifvStuff = gifvStuff + ` onclick="this.classList.toggle('tweet-sens-blur-no')"`;
}
gifvStuff = gifvStuff + `><source src="${Json.media_attachments[0].url}"><p class="legal ctr">(Your browser doesn’t support the <code>video</code> tag.)</p></video>`;
if (Json.sensitive) {
gifvStuff = gifvStuff + `<div class="blur-text">Sensitive content<br />(flagged at origin)</div>`;
}
gifvStuff = gifvStuff + `</div>`
}
})
/*
N.B.:
The above results in an empty, no-height div
when there's no image but there **is**
at least one item in `$media_attachments`.
Unfortunately, it seems to be the only way
to accomplish this. Not a good HTML practice,
but gets the job done.
*/
}
if(Json.card !== null) {
cardStuff = ``;
cardStuff = cardStuff + `<a href="${Json.card.url}" rel="noopener"><div class="card"><img src="${Json.card.image}" alt="Card image from ${instance} toot ${id}" loading="lazy" class="tweet-card-img" /><p><span class="card-title">${Json.card.title}</span><br />${Json.card.description}</p></div></a>`;
}
if (Json.poll !== null) {
votesCount = Json.poll.votes_count;
let pollIterator = 0;
pollStuff = ``;
pollStuff = pollStuff + `<div class="tweet-poll-wrapper">`;
Json.poll.options.forEach(( options ) => {
pollStuff = pollStuff + `<div class="tweet-poll-count"><strong>${((Json.poll.options[pollIterator].votes_count)/(votesCount)).toLocaleString("en", {style: "percent", minimumFractionDigits: 1, maximumFractionDigits: 1})}</strong></div><div class="tweet-poll-meter"><meter id="vote-count" max="${votesCount}" value=${Json.poll.options[pollIterator].votes_count}></meter></div><div class="tweet-poll-title">${Json.poll.options[pollIterator].title}</div>`;
pollIterator = ++pollIterator;
})
pollStuff = pollStuff + `</div><p class="legal tweet-poll-total">${votesCount} votes</p>`;
}
if (Json.content) {
stringToRet = `<blockquote class="tweet-card" cite="${tootLink}" data-pagefind-ignore>
<div class="tweet-header">
<a class="tweet-profile twitterExt" href="https://${instance}/@${Json.account.acct}" rel="noopener"><img src="${Json.account.avatar}" alt="Mastodon avatar for ${handleInst}" loading="lazy" /></a>
<div class="tweet-author">
<a class="tweet-author-name twitterExt" href="https://${instance}/@${Json.account.acct}" rel="noopener">${Json.account.display_name}</a>
<a class="tweet-author-handle twitterExt" href="https://${instance}/@${Json.account.acct}" rel="noopener">${handleInst}</a>
</div>
</div>
<p class="tweet-body">${Json.content}</p>`
if (mediaStuff) {
stringToRet += `<div>${mediaStuff}</div>`
}
if (videoStuff) {
stringToRet += `<div>${videoStuff}</div>`
}
if (gifvStuff) {
stringToRet += `<div>${gifvStuff}</div>`
}
if (cardStuff) {
stringToRet += `<div>${cardStuff}</div>`
}
if (pollStuff) {
stringToRet += `<div>${pollStuff}</div>`
}
let timeToFormat = Json.created_at
let formattedTime = DateTime.fromISO(timeToFormat, { zone: "utc" }).toFormat("h:mm a • MMM d, yyyy")
stringToRet += `<div class="tweet-footer">
<a href="https://${instance}/@${Json.account.acct}/${Json.id}" class="tweet-date twitterExt" rel="noopener">${formattedTime}</a> <span class="pokey">(UTC)</span>
</div>
</blockquote>`
}
return stringToRet
}
Latest commit (d78b94cd
) for page file:
2023-04-11 at 1:41:41 PM CDT.
Page history