I have steadily been working on migrating this website to Zola, and documenting parts of the process which I think might be valuable for others who want to do the same in the future.
On the previous version of this website, I used a Hugo shortcode to display the last 3 posts from my Mastodon account on the landing page.
The shortcode rendered each Mastodon post differently depending on whether or not it had one or more pieces of attached media such as images and videos.
(warning: it ain't pretty)
{{ if .Get "url" }}
{{ $url := .Get "url" }}
{{ $limit := .Get "limit" }}
{{ with resources.GetRemote $url | transform.Unmarshal }}
{{ range first $limit .channel.item }}
<div style="padding: 10px; background-color: #373641; word-wrap: break-word">
{{ $type := (printf "%T" .content) }}
{{ if eq $type "[]interface {}" }}
{{ range .content }}
{{ if index . "-url" }}
{{ $url := index . "-url" }}
{{ if hasSuffix $url "mp4" }}
<video controls style="width: 100% !important; height: auto !important;">
<source src="{{ $url }}">
</video>
{{ else }}
<img src="{{ $url }}">
{{ end }}
{{ end }}
{{ end }}
{{ else }}
{{ if index .content "-url" }}
{{ $url := index .content "-url" }}
{{ if hasSuffix $url "mp4" }}
<video controls style="width: 100% !important; height: auto !important;">
<source src="{{ $url }}">
</video>
{{ else }}
<img src="{{ $url }}">
{{ end }}
{{ end }}
{{ end }}
{{ if .description }}
{{ .description | safeHTML }}
{{ end }}
<p>(<a href="{{ .link }}">View</a>)</p>
</div>
<br />
{{ end }}
{{ end }}
{{ end }}
Below is what an Item entry for a post that has attached media looks like.
Notably, it contains a media:content with url, type, fileSize and
medium attributes.
<item>
<guid isPermaLink="true">https://hachyderm.io/@LGUG2Z/115543625965938161</guid>
<link>https://hachyderm.io/@LGUG2Z/115543625965938161</link>
<pubDate>Thu, 13 Nov 2025 17:43:36 +0000</pubDate>
<description><p>Wild how often I&#39;m being asked if komorebi is currently on or will in the future be coming to Linux</p></description>
<media:content url="https://media.hachyderm.io/media_attachments/files/115/543/625/239/633/063/original/b3acc14a489784cb.png" type="image/png" fileSize="40820" medium="image">
<media:rating scheme="urn:simple">nonadult</media:rating>
</media:content>
</item>
My first pass at translating this shortcode for Zola looked like this.
{% if url %}
{% set feed_data = load_data(url=url, format="xml") %}
{% set items = feed_data.rss.channel.item %}
{% set limit = limit | default(value=3) %}
{% for item in items | slice(end=limit) %}
<div style="word-wrap: break-word">
{% if item["media:content"] %}
{% set media_content = item["media:content"] %}
{% if media_content is iterable and media_content.url is not defined %}
{% for media_item in media_content %}
{% if media_item.url %}
{% set media_url = media_item.url %}
{% if media_url | split(pat=".") | last == "mp4" %}
<video controls style="width: 100% !important; height: auto !important;">
<source src="{{ media_url }}">
</video>
{% else %}
<img src="{{ media_url }}" style="max-width: 100%; height: auto;">
{% endif %}
{% endif %}
{% endfor %}
{% else %}
{% if media_content.url %}
{% set media_url = media_content.url %}
{% if media_url | split(pat=".") | last == "mp4" %}
<video controls style="width: 100% !important; height: auto !important;">
<source src="{{ media_url }}">
</video>
{% else %}
<img src="{{ media_url }}" style="max-width: 100%; height: auto;">
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% if item.description %}
{{ item.description | safe }}
{% endif %}
<p>
(<a href="{{ item.link }}">View</a>)
</p>
</div>
<br />
{% endfor %}
{% endif %}
I thought everything was working because at the time, my recent posts did not contain any media attachments. Today however, I noticed that a media attachment I expected to see rendered with a post was not there.
After a little manual debugging to see if the {% if item["media:content"] %}
clause ever returned true (it didn't) and trying different variations of the
media:content key to try and access that data in the RSS Item, I decided to
just take a look at how xml data is deserialized and reserialized in Zola.
graph LR
A[Zola] --> B[Tera]
B --> C[quickxml_to_serde]
I quickly found an open
issue in the
quickxml_to_serde repo which demonstrated exactly what was happening in the
serialization process: namespace prefixes (like our media: were getting
silently dropped, and additionally, attributes like url were being prefixed
with @.
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">http://redacted.redacted.com/redacted/redacted/StartUsage</a:Action>
<a:MessageID>urn:uuid:15aea56f-7690-467c-8d6b-145f64cd2747</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">sb://redacted.servicebus.windows.net/redacted/90f791da-dc1e-49c9-a55c-dfd8ad4398ee/</a:To>
</s:Header>
<s:Body>
<StartUsage xmlns="http://redacted.redacted.com/redacted/"/>
</s:Body>
</s:Envelope>
{
"Envelope": {
"Body": {
"StartUsage": {}
},
"Header": {
"Action": {
"#text": "http://redacted.redacted.com/redacted/redacted/StartUsage",
"@s:mustUnderstand": 1
},
"MessageID": "urn:uuid:15aea56f-7690-467c-8d6b-145f64cd2747",
"ReplyTo": {
"Address": "http://www.w3.org/2005/08/addressing/anonymous"
},
"To": {
"#text": "sb://redacted.servicebus.windows.net/redacted/90f791da-dc1e-49c9-a55c-dfd8ad4398ee/",
"@s:mustUnderstand": 1
}
}
}
}
With this new knowledge in hand, I was able to update my shortcode to correctly handle Mastodon posts with media attachments.
{% if url %}
{% set feed_data = load_data(url=url, format="xml") %}
{% set items = feed_data.rss.channel.item %}
{% set limit = limit | default(value=3) %}
{% for item in items | slice(end=limit) %}
<div style="word-wrap: break-word">
{% if item["content"] %}
{% set media_content = item["content"] %}
{% if media_content is iterable and media_content["@url"] is not defined %}
{% for media_item in media_content %}
{% if media_item["@url"] %}
{% set media_url = media_item["@url"] %}
{% if media_url | split(pat=".") | last == "mp4" %}
<video controls style="width: 100% !important; height: auto !important;">
<source src="{{ media_url }}">
</video>
{% else %}
<img src="{{ media_url }}" style="max-width: 100%; height: auto;">
{% endif %}
{% endif %}
{% endfor %}
{% else %}
{% if media_content["@url"] %}
{% set media_url = media_content["@url"] %}
{% if media_url | split(pat=".") | last == "mp4" %}
<video controls style="width: 100% !important; height: auto !important;">
<source src="{{ media_url }}">
</video>
{% else %}
<img src="{{ media_url }}" style="max-width: 100%; height: auto;">
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% if item.description %}
{{ item.description | safe }}
{% endif %}
<p>
(<a href="{{ item.link }}">View</a>)
</p>
</div>
<br />
{% endfor %}
{% endif %}
If you have any questions or comments you can reach out to me on Bluesky and Mastodon.
If you're interested in what I read to come up with solutions like this, you can subscribe to my Software Development RSS feed.
If you'd like to watch me writing code while explaining what I'm doing, you can also subscribe to my YouTube channel.
If you would like early access to komorebi for Mac, you can sponsor me on GitHub.