Skip to content
Article Audio track
☀️ Light Clean and bright
📜 Sepia Warm and vintage
🌤️ Light Gray Subtle and neutral
🔵 Light Blue Calm and serene
🌙 Dark Easy on the eyes
🌑 Dark Gray Deep and modern
🔷 Dark Blue Professional and sleek
🌿 Forest Calm and natural

Audio track front matter

Every field below goes between the --- fences at the top of an episode page. Nothing here is stored in a database — the whole track is this block plus the markdown body underneath it. The only thing the engine keeps outside the file is the like count (AudioLikeStore), and comments, which come from the existing page comments API.

The example this documents:

---
title: How the engine got started
audio: true
file: https://sepodesk.com/wp-content/plugins/sepodesk/Storage/Uploads/4c068810-7939-428a-b4a0-0b780adaebfe/6a8ef822d5b6b.mp3
image: /img/w1.jpeg
duration: 32:10
episode: 1
season: 1
published: 2026-08-01
host: Chanda M
guest: Dr Banda
summary: One line for the list view.
tags: build, history
download_requires_login: true
allow_download: true
explicit: false
---

The two that decide whether the partial runs at all

audio: true

Marks the page as a track. Without it (or without the page sitting in an audio/, podcast/ or episodes/ folder) audio.php returns immediately and the page renders as ordinary docs.

It is a flag, not a path. If you write audio: true the partial explicitly ignores that value when looking for a file — the file comes from file:. You can drop this line entirely if the page lives in audio/; folder position is enough.

Accepted equivalents: none — it is either this key or the folder.

file:

The actual audio. Two forms behave differently:

Absolute URL (what your example uses)

file: https://sepodesk.com/.../6a8ef822d5b6b.mp3

The player uses it as-is. If download_requires_login is on, the download route authenticates the listener and then sends a 302 to this URL — it never streams the bytes itself, because it has no local file to read.

Site-relative path

file: /public/audio/ep1.mp3

Passed through $linkUrl, so it picks up the WordPress base path automatically. For a gated download the route resolves it on disk, searching public/, Content/Pages/, Content/Audio/, Storage/Uploads/ and the base path in that order, then streams it with byte-range support so seeking works.

Aliases: audio_file, src, track.

Worth knowing given your URL: a public absolute URL stays public. See A note on download_requires_login at the end.


Display fields

title:

The episode heading, the link text in the list, and the label in the sticky player dock. Falls back to the filename if missing.

image:

Artwork. Square works best — 180×180 on the episode page, 66×66 in the list, both object-fit: cover, so a wide image gets cropped to its centre. Run through $assetUrl, so /img/w1.jpeg resolves against your install root. Omit it and the slot renders as a neutral grey block rather than a broken image.

Aliases: artwork, cover.

duration: 32:10

Display only, and only until the file loads. It is what the listener sees before playback starts; the moment the browser reads the file's metadata the player replaces it with the real duration. Nothing validates it, so a wrong value here is a cosmetic lie that corrects itself on play. Free text — 32:10, 1:04:22 and 32 min all render.

Alias: length.

episode: 1 / season: 1

Both are strings for display, joined into the byline as Ep 1 · Season 1 · …. They do not sort the list — that's published and order. Leave season out for a show that doesn't run seasons and it simply won't appear.

Alias for episode: ep.

published: 2026-08-01

Two jobs. Displayed as 1 Aug 2026, and used to sort the index newest first. Parsed with strtotime, so 2026-08-01, 1 August 2026 and 2026-08-01 09:00 all work; ISO is the safe choice. An episode with no published sorts to the bottom.

Alias: date.

host: / guest:

Byline under the title — "Hosted by Chanda M · With Dr Banda". guest is also included in the index search text, so typing a guest's name filters the list to their appearances. host is not searched (it's usually the same on every episode).

Alias for host: author.

summary:

One line. Shown under the title on the episode page and appended after the stamp in the list row, clamped to two lines. Also searchable. Keep it to a sentence — this is the line someone skims when deciding what to play.

Alias: description.

tags: build, history

Comma or pipe separated, or a YAML list. Rendered as pills on the episode page and folded into the index search text. They are labels, not links — there is no tag archive page behind them.

group: Web Development

Puts the episode under a heading in the listing, and marks it as part of a series. Free text — whatever you write becomes the heading, matched case-insensitively, so Web Development and web development are the same group. Leave it off and the episode falls under "Other episodes" at the bottom.

On the episode page the group shows as a coloured chip, and "More episodes" prefers other tracks from the same group before falling back to the rest of the folder. In the listing it also becomes a filter pill and is searchable, so typing "web" narrows to that series.

Aliases: series, category, collection.

explicit: false

true puts a small E badge next to the title in both views. Nothing else changes; it does not gate playback or hide the episode.


Download behaviour

allow_download: true

Default is true, so you only ever need to write this to turn downloads off. When false the download button disappears from both views, and the /audio/download route refuses with a 403. Playback is unaffected — this hides the download, not the episode.

Alias: download.

download_requires_login: true

Default false. Changes what the download button points at:

Listenerfalse (default)true
Signed outdirect link to the file"Sign in to download" → login page
Signed indirect link to the file/audio/download?track=<slug>, behind AuthMiddleware

Aliases: login_to_download, requireLogin.


Sorting

The listing sorts by group, then order, then published newest-first, then title. Group comes first so a series' episodes stay together — the rest only decides the order inside a group.

order: (optional, not in the example)

An integer, default 500, applied before the date sort. Everything at the default falls back to newest-first, which is what a podcast usually wants. Set order: 1 on one episode to pin it to the top of its group — a trailer, or the one you want new listeners to start with.


The index page

The listing is a separate page. Only audio_dir is required:

---
title: Podcast
audio_dir: audio
per_page: 10
group_order: Web Development|Design|Ops
ungrouped_label: Other episodes
---

audio_dir — the folder to scan. Files starting with _ or index. are skipped, so audio/index.md listing audio/ doesn't list itself. Anything you write in the body renders above the episode list.

per_page — episodes per page, default 10. Paging is client-side: every episode is in the HTML and the page shows a window of it, so searching and filtering re-page instantly with no round trip. A group that spans two pages gets its heading repeated at the top of the second one.

group_order — the order headings appear in, pipe or comma separated. Any group you don't name here sorts alphabetically after the ones you do, and ungrouped episodes always come last. Names match case-insensitively.

ungrouped_label — the heading for episodes with no group:, default "Other episodes". Only appears when at least one other episode is grouped; if nothing is grouped, the listing has no headings at all.


Download counts

Nothing to configure — every download is counted, and the number sits next to the download button in both views. AudioDownloadStore keeps one row per person per track with a running tally (signed-out grabs all share one ~guest row), so a track's file stays the size of its audience rather than growing with every click.

How a grab reaches the counter depends on the track:

  • Gated (download_requires_login: true) — the route serving the bytes counts it. Range requests are ignored, so a player seeking through the file doesn't inflate the number.
  • Direct — the file link never touches PHP, so the page posts a beacon to /api/v2/audio/downloaded as it navigates.

Either way a session only counts once per track per minute, which keeps a double-click or a retrying client from turning into a write storm.


A note on download_requires_login

With your file: pointing at an absolute https://sepodesk.com/... URL, that URL is fetchable by anyone who has it, signed in or not. The player puts it in the page's HTML so playback works — which means the gate hides the download button, not the file. Anyone who opens devtools can still grab the mp3.

That's a deliberate trade: you said listening should be open, and streaming through PHP costs a worker process per listener where a direct URL costs nothing.

If you do want the audio itself behind the login, two changes:

  1. Move the file somewhere the web server won't serve directly — under Content/Audio/ rather than a public Storage/Uploads/ URL — and write it as a relative path.
  2. Point the player at the gated route for those tracks: /audio/download?track=<slug>&stream=1. The route already handles Range requests, so seeking and scrubbing keep working.

Do that only for the episodes that need it. Every gated stream is a PHP process held open for the length of the episode, which on shared hosting is exactly the kind of thing that exhausts the worker pool.