Wavecast Radio

The sticky footer player (<podcast-footer>) must be present in your site’s base template for cross-page persistence.

Add this element just before the closing </body> tag in layouts/_default/baseof.html:

<podcast-footer id="podcast-footer"
  data-turbolinks-permanent
  data-turbo-permanent
  hx-preserve>
</podcast-footer>

How Persistence Works

The footer survives page navigations using two complementary strategies:

  1. Framework DOM preservation (required for seamless playback). One of these frameworks must be loaded on the page:

    • htmx (recommended — smallest, most predictable)
    • Turbo (Hotwire — “Rails-y” feel, more aggressive)
    • Turbolinks 5 (legacy; vendored copy in assets/js/vendor/turbolinks.js)

    Without a framework, the footer is destroyed and recreated on every navigation. The <audio> element is re-instantiated with no source and playback stops, even though Wavecast will restore currentTime from sessionStorage on the next page.

  2. State restoration via sessionStorage (always active). On every beforeunload, the footer saves {currentTime, volume, muted, playbackRate, paused}. On the next page, the inline player on the same source restores position (with a 1-hour staleness guard).

Framework Attributes

The footer element declares which framework to opt into:

FrameworkAttribute on <podcast-footer>What it does
htmxhx-preserveTells htmx to keep the element during hx-boost swaps
Turbodata-turbo-permanentTells Turbo Drive to morph-instead-of-replace the element
Turbolinks 5data-turbolinks-permanentTells Turbolinks to relocate the element into the new body

Wavecast’s JS detects the first available framework on load (window.htmx, window.Turbo, window.Turbolinks) and logs the chosen adapter. The other attributes are harmless no-ops.

Quick setup per framework

htmx (recommended):

<!-- in <head> -->
<script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"
        integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"
        crossorigin="anonymous" defer></script>

<!-- in <body> -->
<body hx-boost="true">

<podcast-footer id="podcast-footer" hx-preserve></podcast-footer>

Turbo:

<!-- in <head> -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.1/dist/turbo.es2017-esm.min.js"></script>

<podcast-footer id="podcast-footer" data-turbo-permanent></podcast-footer>

Turbolinks 5 (uses the copy bundled in the Wavecast module):

<!-- in <head> -->
<script src="{{ "js/vendor/turbolinks.js" | relURL }}"></script>

<podcast-footer id="podcast-footer" data-turbolinks-permanent></podcast-footer>

Without one of these scripts, the persistence attributes are dead code — the browser ignores them, and the footer is re-created on every page load.

Theme Toggle Integration

If your site has a dark/light theme toggle, make sure the footer’s CSS custom properties respond to your theme switching. The <podcast-footer> element responds to these selectors automatically:

/* Built-in selectors the player responds to */
[data-theme="dark"] podcast-footer,
.theme-dark podcast-footer,
html[data-theme="dark"] podcast-footer {
  /* Player applies dark theme variables */
}

If your theme uses different attribute names (e.g., body.dark or [color-scheme="dark"]), add your own rules:

body.dark podcast-footer {
  --podcast-player-bg: #1e1e2e;
  --podcast-player-text: #e0e0e0;
}

body.dark podcast-player {
  --podcast-player-bg: #1e1e2e;
  --podcast-player-text: #e0e0e0;
}

## Scrolling Long Titles

When a playing episode has a title longer than the footer's title area, the
title automatically scrolls horizontally so the full text is visible. Short
titles render normally with no animation. The behavior respects
`prefers-reduced-motion: reduce`, which falls back to ellipsis truncation.

## Linking the Footer

The persistent footer shows a small source label (the audio host's domain) below the episode title. By default this label is a link to the audio file's parent directory. The `url` shortcode parameter overrides or hides the link:

- Omitted, auto-derived: the link points at the audio file's parent directory (e.g. `src="https://example.com/episodes/foo.mp3"` makes the link `https://example.com/episodes/`).
- A URL: the link points at that URL instead. Accepts `http://`, `https://`, and site-relative paths starting with `/`, `#`, or `.`.
- `"none"`: the link is hidden entirely. Useful for live radio streams that have no episode page.

```go-html-template
{{< podcast-player
  src="https://example.com/stream.mp3"
  title="Live Broadcast"
  url="https://example.com/shows/live"
>}}

{{< podcast-player
  src="https://example.com/stream.mp3"
  title="Live Broadcast"
  url="none"
>}}

The same attribute can be set on the top-level <podcast-footer url="..."> to override whatever the inline player sends. URLs are sanitized at build time and at runtime; only http and https schemes are accepted when the link is rendered.

The size attribute on <podcast-footer> controls the bar’s visual footprint. Three presets are available:

  • size="small" (default): the current compact bar. 36px cover, 140px info column, 32px buttons. Omitting the size attribute is equivalent to size="small".
  • size="medium": a wider bar with a 48px cover, 240px info column, 36px buttons, and slightly larger fonts.
  • size="large": a full-bleed banner with a 64px cover, 400px info column, 44-56px buttons, and the largest fonts.
<podcast-footer id="podcast-footer" size="medium" data-turbo-permanent></podcast-footer>

On viewports narrower than 768px, all three sizes collapse to the same compact layout that size="small" produces on desktop. Unknown values (size="huge", etc.) fall back to the default small layout.

The attribute is purely visual. Persistence, playback, and the audio source URL are unaffected by size. Theme variables (--podcast-player-bg, --podcast-player-text, etc.) continue to work as before.

<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
  <head>
    <meta charset="utf-8">
    <title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
    <!-- head content... -->
  </head>
  <body>
    {{ block "main" . }}{{ end }}

    <!-- Footer player -->
    <podcast-footer id="podcast-footer"
      data-turbolinks-permanent
      data-turbo-permanent
      hx-preserve>
    </podcast-footer>
  </body>
</html>

Next Steps