Journal Entry

I created a podcast site, so when I feel like yapping

I built a small podcast site for publishing audio episodes whenever I want to record something and put it online without much setup.

The site works from a simple episode data structure. Each episode has an ID, title, description, audio file URL, cover image, and date. That same data is reused to power the site, the embedded player, and the podcast RSS feed.

How it works

flowchart TD
    A["Episode metadata in JSON"] --> B["Site reads episode data"]
    B --> C["Episode embed page"]
    B --> D["Podcast RSS feed"]
    C --> E["Browser loads audio file"]

A simplified episode entry looks like this:

{
  "id": "1",
  "title": "EKKSTACY - i walk this earth all by myself",
  "description": "Sample uploaded episode using the local .m4a file from public/uploads.",
  "audio_url": "/uploads/EKKSTACY-i-walk-this-earth-all-by-myself.m4a",
  "cover_url": "/assets/cover-1.svg",
  "date": "2026-04-22"
}

This is useful because the site only needs one source of truth for each episode. The same metadata can be used in multiple places instead of rewriting it by hand.

Episode 1

Here is episode 1 embedded directly in the post:

The iframe points to /embed/1, which means the site loads the player for the episode with ID 1.

Technical details

sequenceDiagram
    participant J as Episode JSON
    participant S as Site Logic
    participant E as Embed Route
    participant R as RSS Generator

    J->>S: Load episode metadata
    S->>E: Find episode by ID
    S->>R: Reuse same metadata
    E->>Browser: Render audio player
    R->>PodcastApps: Publish feed items

A simplified version of the lookup code would look like this:

const episodes = [
  {
    id: "1",
    title: "EKKSTACY - i walk this earth all by myself",
    audio_url: "/uploads/EKKSTACY-i-walk-this-earth-all-by-myself.m4a",
    cover_url: "/assets/cover-1.svg",
    date: "2026-04-22"
  }
];

function getEpisodeById(id) {
  return episodes.find((episode) => episode.id === id);
}

Then the embed page can use that result to render the player:

const episode = getEpisodeById("1");

const html = `
  <article>
    <h1>${episode.title}</h1>
    <audio controls src="${episode.audio_url}"></audio>
  </article>
`;

The RSS feed follows the same pattern. Instead of maintaining a separate feed database, the feed generator reads the same episode metadata and turns it into podcast feed items.

Why this setup is useful

graph LR
    A["Single episode object"] --> B["Website page"]
    A --> C["Embedded player"]
    A --> D["RSS item"]

This setup keeps the site lightweight. Adding a new episode mostly means uploading an audio file and adding one metadata object. After that, the site can render the episode, expose an embed player, and include it in the podcast feed automatically.

Previous Today I Learned: Why PageRank Works (and it’s just linear algebra)