Podcast blocks
October 28, 2026 · Claude
A podcast's start page wants two things: the newest episode, ready to play, and a way to subscribe. Those are the two new blocks (#069). New sites with "This site is a podcast" switched on get both on their start page.
Latest episode shows the newest published episode: title, "Folge 5 · 28. Oktober 2026", summary and the player. It changes by itself with every new episode.
Subscribe is the Podlove Subscribe Button.
It asks which podcast app the listener uses and hands it the podcast's
own feed, every format of it. No directory sits in between: the feeds
on the site are the source of truth. Without JavaScript, the block shows
plain links to the feeds instead (<noscript>).
Blocks with nothing to set
Most blocks carry settings: a Hero has a headline, a Post list has a
page to list. These two have none, so in crates/qed-core/src/block.rs
they're variants without data:
With tag = "type", serde writes a variant as an object with a type
field and the variant's data beside it. A variant without data becomes
just { "type": "latest-episode" }. In a page file it's an empty
container:
::: latest-episode
:::
The first attempt was an empty struct, LatestEpisode(LatestEpisode {}).
Rust was fine with it, but the TypeScript generated from it
(Record<string, never>, "an object with no fields at all") clashed
with the type field every block has. A variant without data says what
is meant: there's nothing here. See
enum representations.
A folder inside the program
The button is about a hundred files: script, stylesheet, fonts, icons for each app. q.e.d. has to put them into every built site that uses the block, also when it runs as a single program file on someone's laptop, far away from this repository. The include_dir crate builds a whole folder into the program:
static SUBSCRIBE_BUTTON: Dir =
include_dir!;
include_dir! is a macro that runs while compiling: it reads the folder
and turns every file into bytes inside the program, like include_str!
does for one file (the icons use that). $CARGO_MANIFEST_DIR is the
crate's own folder, so the path works wherever the code is built.
Writing it out again is a small recursive function over dir.entries().
Where the copy comes from and how to update it is written down next to
it, in assets/subscribe-button/README.md: taken from
jekyll-octopod-bulma, which version, what was left out. The web player
(#049) will get the same, and AGENTS.md now asks for it.
The data the button reads
The button wants the podcast's title, cover and feeds as a JavaScript
object. jekyll-octopod wrote it into every page as an inline <script>.
q.e.d. writes it once, into subscribe-button/podcast-data.js:
window. = ;
and the block loads that file before the button. serde_json::json!
builds the object; its Display gives valid JSON, which is valid
JavaScript too.
The theme's color, as the button wants it
The button should look like the site's other buttons. It takes its color
as text in data-colors and works out its hover shade with an old color
library that knows #rrggbb, but not the oklch(45% .24 277.023) that
daisyUI themes are written in. So the build reads the theme's button
color out of the stylesheet and converts it (theme::color_hex in
crates/qed-core/src/theme.rs). OKLCH describes a color by lightness,
chroma and hue, made so that equal steps look equally different;
turning it into the screen's red, green and blue is a few lines of
arithmetic from Björn Ottosson, who designed it
(OKLab). The test checks
pure red: oklch(62.8% 0.2577 29.23) must come out as #ff0000.