The Problem
Embed Mastodon posts (toots) into Hugo content for context.
The Solution
Bryce Wray provides the code. But this didn’t look right on my PaperMod theme. So I tried something different.
Put this into layouts/shortcodes/toot.html
:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{{/* layouts/shortcodes/toot.html */}} | |
<style> | |
.toot { | |
--toot-bg: var(--entry); | |
--toot-border: rgba(0,0,0,0.1); | |
--toot-radius: 12px; | |
--toot-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
background: var(--toot-bg); | |
border: 1px solid var(--toot-border); | |
border-radius: var(--toot-radius); | |
box-shadow: var(--toot-shadow); | |
padding: 16px; | |
margin: 1.5rem auto; | |
max-width: 600px; | |
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif; | |
} | |
.dark .toot { | |
--toot-border: rgba(255,255,255,0.1); | |
--toot-shadow: 0 1px 3px rgba(0,0,0,0.2); | |
} | |
.toot-header { | |
display: flex; | |
align-items: flex-start; | |
gap: 12px; | |
margin-bottom: 12px; | |
} | |
.toot-profile img { | |
width: 48px; | |
height: 48px; | |
border-radius: 4px; | |
display: block; | |
} | |
.toot-author { | |
display: flex; | |
flex-direction: column; | |
gap: 2px; | |
} | |
.toot-author-name { | |
color: var(--primary); | |
font-weight: 700; | |
text-decoration: none; | |
font-size: 15px; | |
line-height: 20px; | |
} | |
.toot-author-handle { | |
color: var(--secondary); | |
text-decoration: none; | |
font-size: 14px; | |
line-height: 18px; | |
} | |
.toot-content { | |
color: var(--content); | |
font-size: 15px; | |
line-height: 1.5; | |
margin: 12px 0; | |
} | |
.toot-content p { | |
margin: 0 0 12px; | |
} | |
.toot-content p:last-child { | |
margin-bottom: 0; | |
} | |
.toot-content a { | |
color: #2b90d9; | |
text-decoration: none; | |
} | |
.toot-content a:hover { | |
text-decoration: underline; | |
} | |
.toot-media-grid { | |
display: grid; | |
grid-gap: 8px; | |
margin: 12px 0; | |
border-radius: 12px; | |
overflow: hidden; | |
} | |
.toot-media-grid[data-count="1"] { | |
grid-template-columns: 1fr; | |
} | |
.toot-media-grid[data-count="2"] { | |
grid-template-columns: repeat(2, 1fr); | |
} | |
.toot-media-grid[data-count="3"] { | |
grid-template-columns: repeat(2, 1fr); | |
} | |
.toot-media-grid[data-count="3"] > :first-child { | |
grid-column: 1 / -1; | |
} | |
.toot-media-grid[data-count="4"] { | |
grid-template-columns: repeat(2, 1fr); | |
} | |
.toot-media-item img { | |
width: 100%; | |
height: auto; | |
display: block; | |
border-radius: 4px; | |
} | |
.toot-footer { | |
margin-top: 12px; | |
padding-top: 12px; | |
border-top: 1px solid var(--toot-border); | |
color: var(--secondary); | |
font-size: 14px; | |
} | |
.toot-date { | |
color: inherit; | |
text-decoration: none; | |
} | |
.toot-date:hover { | |
text-decoration: underline; | |
} | |
</style> | |
{{ $masIns := .Get "instance" }} | |
{{ $id := .Get "id" }} | |
{{ $tootLink := "" }} | |
{{ $handleInst := "" }} | |
{{ $urlToGet := print "https://" $masIns "/api/v1/statuses/" $id }} | |
{{ with resources.GetRemote $urlToGet }} | |
{{ $json := .Content | unmarshal }} | |
{{ if isset $json "account" }} | |
{{ $tootLink = print "https://" $masIns "@" $json.account.acct "/status/" $id }} | |
{{ $handleInst = print "@" $json.account.acct "@" $masIns }} | |
{{ end }} | |
{{ if isset $json "content" }} | |
<div class="toot"> | |
<div class="toot-header"> | |
<a class="toot-profile" href="https://{{ $masIns }}/@{{ $json.account.acct }}" rel="noopener"> | |
<img src="{{ $json.account.avatar }}" | |
alt="Avatar for {{ $handleInst }}" | |
loading="lazy"> | |
</a> | |
<div class="toot-author"> | |
<a class="toot-author-name" | |
href="https://{{ $masIns }}/@{{ $json.account.acct }}" | |
rel="noopener">{{ $json.account.display_name }}</a> | |
<a class="toot-author-handle" | |
href="https://{{ $masIns }}/@{{ $json.account.acct }}" | |
rel="noopener">{{ $handleInst }}</a> | |
</div> | |
</div> | |
<div class="toot-content">{{ $json.content | safeHTML }}</div> | |
{{ with $json.media_attachments }} | |
{{ $count := len . }} | |
<div class="toot-media-grid" data-count="{{ $count }}"> | |
{{ range . }} | |
{{ if eq .type "image" }} | |
<div class="toot-media-item"> | |
<img src="{{ .url }}" | |
alt="" | |
loading="lazy"> | |
</div> | |
{{ end }} | |
{{ end }} | |
</div> | |
{{ end }} | |
<div class="toot-footer"> | |
<a href="{{ $tootLink }}" | |
class="toot-date" | |
rel="noopener">{{ dateFormat "3:04 PM · Jan 2, 2006" $json.created_at }}</a> | |
</div> | |
</div> | |
{{ end }} | |
{{ else }} | |
<div class="toot"> | |
<p style="text-align: center; color: var(--secondary); margin: 0;"> | |
[Source not online at time of site build.] | |
</p> | |
</div> | |
{{ end }} |
And invoke like this:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{{< toot instance="fosstodon.org" id="113562547547583073" >}} |
The Example
I haven’t been posting a lot on #Mastodon since diving into #Bluesky. One of the things I’ve been enjoying recently is getting more hands-on with #ESP32 & #ESPhome. I’m learning how to pull data from sensors and use attached displays. Also integrating with #HomeAssistant. This prototype is building up towards hacking a Big Mouth Billy Bass novelty toy. But I’d also like to build a really anachronistic digital instrument display for my vintage Harley-Davidson project motorcycle. #makers

