[Home](https://unprompted.dirkprimbs.de/index.md) · [← Previous: They expected her to stay longer](https://unprompted.dirkprimbs.de/posts/they-expected-her-to-stay-longer.md) · [Next: July/August reading →](https://unprompted.dirkprimbs.de/posts/july-august-reading.md)

# My blog engine now publishes podcasts

> The engine grew a podcast feed, a player and a migration path, because I had four shows sitting on a WordPress install I no longer wanted to maintain.

- Published: 2026-08-29
- Tags: [Blogging](https://unprompted.dirkprimbs.de/tags/blogging.md), [Engine Updates](https://unprompted.dirkprimbs.de/tags/engine-updates.md), [Podcasting](https://unprompted.dirkprimbs.de/tags/podcasting.md), [WebDev](https://unprompted.dirkprimbs.de/tags/webdev.md), [OSS](https://unprompted.dirkprimbs.de/tags/oss.md)
- Web page: https://unprompted.dirkprimbs.de/posts/engine-update-podcasts.html

---

Last time I wrote about the engine it had [just learned to talk to two social networks at once](https://unprompted.dirkprimbs.de/posts/engine-update-bluesky.md). This time it learned to publish podcasts, which was motivated by a serious of hacks impacting my wordpress installations.

That was three weeks ago. All four shows have moved. Together that's 162 episodes now being served as flat files by the same thing that publishes this post. So, how does it work? Well, for starters it is important to understand that podcasts always were a special case of a blog. So adding podcast functionality is not as difficult as it sounds at first. However, once you decide to serve podcasts you need to consider a number of questions like how to configure meta data, how to construct the podcast feed etc. So, first things first... how does my engine recognize a podcast? Well, easy: 

### A post that links an audio file is an episode

That's the whole rule. There's no episode type, no separate directory, no mode I switch on. If a post contains a link to an audio file sitting alone on its own line, the engine treats it as an episode - copies the file in, reads its duration, gives it a player, and puts it in the podcast feed. A link to the same file inside a sentence stays a plain link.

I'm fond of this because it isn't a new concept at all, it's two old ones pointed at an mp3. The engine already knew that a link alone on a line means "embed this" - that's how a YouTube URL turns into a click-to-play thumbnail. And it already knew how to copy a referenced file into the site and rewrite the Markdown to point at its copy - that's how images work. Put those together and you have podcast hosting.

### Two feeds, on purpose

There's `/podcast.xml`, which is the feed we need - iTunes tags, Podcasting 2.0 tags, every episode ever, no cap. And the normal blog feed keeps being a blog feed, except episode items now carry an enclosure so anyone already subscribed can just play the thing without subscribing twice. That way my pages can mix content types and still be followed via regular feed while also offering a podcast-only feed with all the relevant meta tags needed for podcatchers and podcast directories. 

Subscribing is Podlove's button, vendored into the theme rather than loaded from someone's CDN, because I didn't want a third party watching who clicks subscribe on a page whose entire point is that nothing third-party loads. Admittedly this was mostly on principle because I trust the team behind podlove more than most that they treat data with respect, but, you know, consistency is king :)

### Moving four shows without breaking anyone's app

If you followed this space or my podcasts you may know already: This project was built to take over my wordpress pages eventually. That meant - my existing podlove based shows needed to be imported. Feed forwards, episode GUIDs, media URLs - all needed to be considered so a subscriber ideally does not even notice that I swapped the technical infrastructure underneath. 

Effectively that means: The feed URL is the thing that must never 404. It lives in every subscriber's app and nobody re-subscribes, they just stop hearing from you. So the engine grew a `redirects:` list in the config - old path to new path - which becomes 301s at the top of the generated `.htaccess`, and a way to pin the show's own identifier to the one it already published, since a show doesn't become a different show by moving house.

The rest of that work was traps, and I wrote most of them down in a migration guide because I hit each one exactly once and would otherwise have hit them again on show number three. Apache's MultiViews quietly answering the old feed URL itself before my redirect got a look at it. Old WordPress URLs with umlauts in them, which arrive encoded by one browser and not by another. Old URLs with spaces in them, same story. A redirect matching against the rewritten path instead of what the client actually asked for. And the favicon, which stayed the engine's default for an embarrassing number of hours because I'd copied everything else and never looked at the tab.

### The player, and what happens when it fails

The markup ships a real `<audio controls>` element. The script hides it only after it has successfully taken over and drawn its own thing. Get that backwards and a broken script leaves you with an episode page containing no way to hear the episode, which is the one failure a podcast page must not have.

You can click the timeline to seek, which is also how I found out that the preview server I'd been using to look at the site locally was lying to me. Initially I considered shipping the podlove player just like the subscribe button but ended with my own thing because it got me half way to my next feature: 

### Shared episodes that play where they're shared

Post an episode link to Mastodon and the card in the timeline has a play button in it. That's oEmbed, with the payload written out as a static file per episode rather than served from an endpoint, and a bare page holding nothing but the player which the card loads in a frame. For this to work the payload has to claim it's a video, because oEmbed has no audio type and only video cards get embedded at all. Theoretically the frame autoplays, so the click that opened the card is the gesture the audio starts on - a browser that refuses to hand that gesture over just leaves the play button sitting there, which is fine.

### The rest, quickly

Episodes announce themselves with `#podcast` rather than `#blog`, decided by whether the post actually has an enclosure - a post tagged as a podcast that no app can subscribe to is worse than not splitting the tag at all. Dates in frontmatter are read as UTC instead of whatever timezone the machine doing the build happens to be in. Sitemap URLs are percent-encoded. Announcements that a long tag list pushed past Mastodon's character limit get trimmed instead of rejected. Images referenced by raw HTML get captioned and copied like Markdown ones do. And the podcast assets no longer ship to sites that have no podcast, which is most of them.

In the end - every single time I go to bed thinking I may have arrived at all the features I could possible want for now just to wake up the next morning with another set of things I like to look at. This is fun for now and it feels really good to have successfully freed myself from the burden of constant wordpress management...
