Frontend

Django Cast ships a small JavaScript layer that powers interactive features: an audio player, an image gallery modal, and AJAX-based comments. Assets are built with Vite and integrated into Django templates via django-vite. Page-level interactivity (pagination, gallery navigation) uses HTMX.

Architecture Overview

The JavaScript source lives in javascript/src/ and is organized into three independent entry points:

src/gallery/image-gallery-bs4.ts

A custom element (<image-gallery-bs4>) that provides Bootstrap-modal gallery navigation with keyboard support. Used by the bootstrap4 theme.

src/audio/podlove-player.ts

A custom element (<podlove-player>) that lazy-loads the Podlove Web Player 5 for audio playback. Used by all themes.

src/comments/ajaxcomments.ts

An IIFE script that intercepts comment form submissions and posts them via fetch, with preview support, threaded replies, and error display. Loaded on post detail pages when comments are enabled.

The plain theme does not use the <image-gallery-bs4> web component for galleries. Instead, it renders a pure HTMX-driven gallery modal (see Gallery Modal Navigation).

Pagination

The blog index page comes with pagination support. You can set the number of posts per page using the POST_LIST_PAGINATION setting.

If there are more than 3 pages, there will be a “…” in the pagination. If there are more than 10 pages, there will be two “…” in the pagination.

Web Components

<podlove-player>

A custom HTML element that wraps the Podlove Web Player 5. It handles lazy loading, dark mode detection, and an optional click-to-load facade.

Template Usage

The element is rendered by the shared template cast/audio/audio.html:

<podlove-player
  id="audio_{{ value.pk }}"
  data-variant="xl"
  data-url="{% url 'cast:api:audio_podlove_detail' pk=value.pk post_id=page.pk %}"
  data-embed="{% static 'cast/js/web-player/embed.5.js' %}"
  data-config="{% url 'cast:api:player_config' %}"
  {% if podlove_load_mode == "facade" %}
    data-load-mode="facade"
  {% elif podlove_load_mode == "click" %}
    data-load-mode="click"
  {% endif %}
>
  {% if podlove_load_mode == "facade" %}
    <!-- server-rendered facade: cover art, title, play button -->
  {% endif %}
</podlove-player>

Data Attributes

data-url (required)

The API endpoint returning the Podlove episode JSON for this audio file.

data-config

URL for the player configuration (theme colors, fonts). Defaults to /api/audios/player_config/. You can customize the theme per template base directory using the CAST_PODLOVE_PLAYER_THEMES setting.

data-embed

URL of the Podlove Web Player embed script. Defaults to the CDN version at https://cdn.podlove.org/web-player/5.x/embed.js. Django Cast ships a local copy at cast/js/web-player/embed.5.js.

data-template

Optional Podlove template name passed through to the player initialization.

data-load-mode

Controls how the player is initialized. Two values are supported:

  • "click" — displays a “Load player” button; the player loads only when clicked. Used on list pages to avoid loading multiple heavy player instances at once.

  • "facade" — the server renders a static preview (cover art, title, play button) inside the element. The player auto-initializes via IntersectionObserver and is injected into the existing container. Used on post detail pages.

If omitted, the player auto-initializes with a plain placeholder.

Initialization Behavior

Auto-init (no data-load-mode, or data-load-mode="facade"):

  1. The element waits for the page load event.

  2. A shared IntersectionObserver (with a 200 px root margin) watches the element.

  3. When the element enters the viewport, initialization is scheduled via requestIdleCallback.

  4. The embed script is loaded (once, shared across all players on the page), then the Podlove player is created inside the element.

In facade mode, the server-rendered preview (cover art, title, decorative play button and progress bar) is visible while the player loads. The JS skips creating its own placeholder when it detects a .podlove-player-container already present inside the element. The player <div> is appended into the existing container; the facade markup remains in the DOM unless hidden by CSS or overwritten by the player iframe.

Click-to-load (data-load-mode="click"):

  1. A placeholder container with a “Load player” button is rendered immediately.

  2. Clicking the button bypasses the page-load wait and IntersectionObserver steps — it directly schedules initialization via requestIdleCallback, then loads the embed script and creates the player.

  3. On failure, an error message is shown and the button text changes to “Try again”.

Dark Mode

The player automatically detects dark mode by checking (in order):

  1. data-bs-theme on <html>

  2. data-theme on <html>

  3. data-bs-theme on <body>

  4. data-theme on <body>

  5. prefers-color-scheme: dark media query

When dark mode is detected, ?color_scheme=dark is appended to the config URL so the server can return appropriate theme tokens.

AJAX Comments

The comments script (ajaxcomments.ts) provides client-side comment posting without full page reloads. It is built as a standalone IIFE and loaded via a <script> tag on post detail pages when comments are enabled.

How It Works

  1. On DOMContentLoaded, the script finds all form.js-comments-form elements and wraps them in position-tracking containers.

  2. Form submissions are intercepted via a delegated submit event listener on document.

  3. The form data is sent via fetch with X-Requested-With: XMLHttpRequest to the form’s data-ajax-action or action URL.

  4. On success, the new comment HTML is inserted into the DOM and the page scrolls to it.

  5. On error, field-level error messages are displayed using Bootstrap has-error / error classes.

Features

  • Preview: Clicking a button with name="preview" shows a preview of the comment without posting it.

  • Threaded replies: Clicking a .comment-reply-link moves the form under the target comment and sets the parent field.

  • Cancel reply: Clicking .comment-cancel-reply-link resets the form back to its original position.

  • Moderation message: If the comment requires moderation, a temporary message is shown for 4 seconds.

  • Error event: On network failure, a cast:comments:error custom event is dispatched on window. If no listener calls preventDefault(), a fallback alert() is shown.

Template Integration

Comments are loaded conditionally in post templates:

{% if comments_are_enabled %}
  <link rel="stylesheet" href="{% static 'fluent_comments/css/ajaxcomments.css' %}" />
  <script defer src="{% static 'fluent_comments/js/ajaxcomments.js' %}"></script>
{% endif %}

The script works on HTMX-navigated pages because it re-wraps forms on each submit, reply, and cancel interaction (not via HTMX lifecycle events). This means forms inserted after the initial page load are handled correctly.

Vite Build Setup

JavaScript assets are built with Vite. There are two separate build configurations in the javascript/ directory.

Comments Build

Configuration file: javascript/vite.comments.config.ts

Entry point:
  src/comments/ajaxcomments.ts  →  ajaxcomments.js

Output:  src/cast/static/fluent_comments/js/
Format:  IIFE (immediately invoked function expression)
Target:  ES2015

The comments build outputs directly into the static files directory. It uses IIFE format (not ES modules) so it works as a simple <script> tag without module loading. Minification is disabled so the output remains readable for debugging.

Build Commands

Run from the javascript/ directory:

npm run build           # Main build (gallery + player)
npm run build:comments  # Comments build
npm run build:all       # Both builds
npm run dev             # Vite dev server for HMR

Or from the project root using the justfile:

just js-build-vite      # Main build + copy to static
just js-build-comments  # Comments build
just js-build-all       # Both builds

Note

just js-build-vite handles the full pipeline: builds with Vite, moves the manifest file, and copies the output to src/cast/static/cast/vite/.

Testing

JavaScript tests use Vitest with a jsdom environment:

cd javascript
npm test             # Run tests once
npm run test:watch   # Watch mode
npm run coverage     # With coverage report

HTMX Integration

Django Cast uses HTMX for two main features: gallery modal navigation and pagination with view transitions.

CSRF Token Handling

The bootstrap4 base template sets a global HTMX CSRF header on the <body> tag:

<body data-hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>

This ensures all HTMX requests automatically include the CSRF token without additional configuration.

Note

The plain theme does not set this header because its HTMX interactions (gallery modals, pagination) only use GET requests. If you add HTMX-driven POST requests to a plain-theme template, add data-hx-headers to your base template or individual elements.

Pagination with View Transitions

Pagination links use HTMX to swap page content without a full page reload. The data-hx-swap attribute includes transition:true to enable CSS view transitions:

<a data-hx-get="?page=2"
   data-hx-target="#paging-area"
   data-hx-swap="innerHTML show:window:top transition:true"
   data-hx-sync="#paging-area:replace"
   data-hx-push-url="true"
   href="?page=2">

Key attributes:

data-hx-target="#paging-area"

Swaps the content of the #paging-area container, which wraps the post list and pagination controls.

data-hx-swap="innerHTML show:window:top transition:true"

Replaces the inner HTML, scrolls to the top of the window, and uses CSS view transitions for a smooth visual effect.

data-hx-sync="#paging-area:replace"

If a new pagination request arrives while one is in-flight, the old request is cancelled.

data-hx-push-url="true"

Updates the browser URL so the Back button works as expected.

A small companion script (paging-view-transition-fix.js) listens for htmx:beforeTransition events targeting the #paging-area element and scrolls to the top of the page before the view transition snapshot is taken, ensuring a clean transition. Other HTMX transitions are not affected.