Apparently, Bluesky is having another surge. Fair enough, given what’s happening on Twitter and in the world at large.
I fully support leaving Twitter1, especially for journalists and similar professionals. Why would you continue publishing on and supporting a platform that actively despises you?
I’m delighted to be on Mastodon, but Bluesky is certainly better than staying on Twitter. For example, it has a useful API that allows us to embed Bluesky posts directly without loading third-party scripts:
New post...
A simple Hugo shortcode for embedding Bluesky posts • While it doesn’t do static embeds, this shortcode gives you an easy way to show content from an increasingly popular social network.
#WebDev #Hugo #Bluesky #oEmbed
As you can see, this embeds the post statically. The shortcode I built—just like the one I made for Mastodon—doesn’t (yet) support all the intricacies of Bluesky posts. In its current state, it handles text posts and link embeds, mostly to demonstrate to Bryce Wray that it’s possible. In fact, my Mastodon shortcode was largely inspired by Bryce Wray’s Mastodon shortcode.
So here it is: a (very incomplete) Hugo shortcode2 for Bluesky posts.
{{ $avatarSize := 42 }}
{{ $imgWidth := 624 }}
{{ $username := .Get "username" }}
{{ $postId := .Get "postId" }}
{{ $uri := print "at://" $username "/app.bsky.feed.post/" $postId }}
{{ $urlToGet := print "https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=" $uri }}
{{- with resources.GetRemote $urlToGet -}}
{{ if .Err }}
<blockquote class="bluesky">
<p class="ctr legal">[Source not online<br />
at time of site build.]</p>
</blockquote>
{{ else }}
{{ $json := unmarshal .Content }}
{{ $thread := $json.thread }}
{{ $post := $thread.post }}
{{ $author := $post.author }}
{{ $record := $post.record }}
{{ $embed := $post.embed }}
{{ $authorUrl := printf "https://bsky.app/profile/%s" $author.did }}
<blockquote class="bluesky not-prose">
<div class="header">
{{ $avatar := resources.GetRemote $author.avatar }}
<a class="profile" href="{{ $authorUrl }}" rel="noopener">
<picture>
<source
srcset="
{{- range $.Site.Params.pixeldensities -}}
{{- if le (int (mul . $avatarSize)) (mul 1.5 $avatar.Width) -}}
{{ ( ( printf "%dx webp" (int (mul . $avatarSize)) ) | $avatar.Resize ).RelPermalink }} {{ . }}x,
{{- end -}}
{{- end -}}"
type="image/webp"
/>
<source
srcset="
{{- range $.Site.Params.pixeldensities -}}
{{- if le (int (mul . $avatarSize)) (mul 1.5 $avatar.Width) -}}
{{ ( ( printf "%dx" (int (mul . $avatarSize)) ) | $avatar.Resize ).RelPermalink }} {{ . }}x,
{{- end -}}
{{- end -}}"
type="{{ $avatar.MediaType.Type }}"
/>
{{ with (printf "%dx" $avatarSize | $avatar.Resize) }}
<img src="{{ .RelPermalink }}" alt="Bluesky avatar for {{ $author.displayName }}" width="{{ .Width }}" height="{{ .Height }}">
{{ end }}
</picture>
</a>
<div class="author">
<a class="name" href="{{ $authorUrl }}" rel="noopener">{{ $author.displayName }}</a>
<a class="handle" href="{{ $authorUrl }}" rel="noopener">@{{ $author.handle }}</a>
</div>
{{ $bskyIcon := resources.Get "icons/bsky.svg" }}
<img src={{ ($bskyIcon | fingerprint).RelPermalink }} class="icon" alt="Bluesky Icon" />
</div>
<div class="content">
{{ (replace $record.text "\n" "<br>") | safeHTML }}
</div>
{{ with $embed }}
{{ $embedType := index . "$type" }}
{{ if eq $embedType "app.bsky.embed.external#view" }}
<a href="{{ index .external "uri" }}" target="_blank" rel="noopener">
<div class="card">
{{ with $imgUrl := .external.thumb }}
{{ $img := resources.GetRemote $imgUrl }}
<div class="image">
<picture class="card-image">
<source
srcset="
{{- range $.Site.Params.pixeldensities -}}
{{- if le (int (mul . $imgWidth)) (mul 1.5 $img.Width) -}}
{{ ( ( printf "%dx webp" (int (mul . $imgWidth)) ) | $img.Resize ).RelPermalink }} {{ . }}x,
{{- end -}}
{{- end -}}"
type="image/webp"
/>
<source
srcset="
{{- range $.Site.Params.pixeldensities -}}
{{- if le (int (mul . $imgWidth)) (mul 1.5 $img.Width) -}}
{{ ( ( printf "%dx" (int (mul . $imgWidth)) ) | $img.Resize ).RelPermalink }} {{ . }}x,
{{- end -}}
{{- end -}}"
type="{{ $img.MediaType.Type }}"
/>
{{ with $img }}
<img src="{{ .RelPermalink }}" alt="" width="{{ .Width }}" height="{{ .Height }}">
{{ end }}
</picture>
</div>
{{ end }}
<div class="meta">
<span class="title">{{ index .external "title" }}</span>
<span class="description">{{ index .external "description" }}</span>
<a href="{{ (urls.Parse (index .external "uri")).Scheme }}://{{ (urls.Parse (index .external "uri")).Host }}" class="website">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m20.893 13.393-1.135-1.135a2.252 2.252 0 0 1-.421-.585l-1.08-2.16a.414.414 0 0 0-.663-.107.827.827 0 0 1-.812.21l-1.273-.363a.89.89 0 0 0-.738 1.595l.587.39c.59.395.674 1.23.172 1.732l-.2.2c-.212.212-.33.498-.33.796v.41c0 .409-.11.809-.32 1.158l-1.315 2.191a2.11 2.11 0 0 1-1.81 1.025 1.055 1.055 0 0 1-1.055-1.055v-1.172c0-.92-.56-1.747-1.414-2.089l-.655-.261a2.25 2.25 0 0 1-1.383-2.46l.007-.042a2.25 2.25 0 0 1 .29-.787l.09-.15a2.25 2.25 0 0 1 2.37-1.048l1.178.236a1.125 1.125 0 0 0 1.302-.795l.208-.73a1.125 1.125 0 0 0-.578-1.315l-.665-.332-.091.091a2.25 2.25 0 0 1-1.591.659h-.18c-.249 0-.487.1-.662.274a.931.931 0 0 1-1.458-1.137l1.411-2.353a2.25 2.25 0 0 0 .286-.76m11.928 9.869A9 9 0 0 0 8.965 3.525m11.928 9.868A9 9 0 1 1 8.965 3.525" />
</svg>
<span>{{ (urls.Parse (index .external "uri")).Host }}</span></div>
</div>
</div>
</a>
{{ end }}
{{ end }}
<div class="footer">
{{ $date := time.AsTime $record.createdAt }}
{{ $timestamp := $date | time.Format "2006-01-02T15:04:05-07:00" }}
{{ $dateHuman := $date | time.Format ":date_medium" }}
{{ $timeHuman := $date | time.Format "15:04" }}
<a href="https://bsky.app/profile/{{ $post.author.did}}/post/{{ $postId }}" class="post-date" rel="noopener">
<time datetime="{{ $timestamp }}">{{ $dateHuman }} {{ $timeHuman }}</time>
</a>
</div>
</blockquote>
{{ end }}
{{- end -}}
Share and Use
As always, feel free to share and use this shortcode. I hope that one day we’ll have a smarter way to collaboratively create and share these tools. Until then, it’s here for you to enjoy!
I’ve blocked the domains
twitter.com
andx.com
on my network. I don’t need to access them and am tired of being tracked by them on many news sites that still embed tweets. On most pages, this works fine—Twitter embeds are just displayed asblockquote
elements, which is arguably better. But not all sites (looking at you, The Verge):I still haven’t found a great way to format Hugo templates, and syntax highlighting remains a bit of a struggle. ↩︎