Django Cast, Explained Several Years Too Late

Django Cast has powered python-podcast.de since 2018, but I have barely explained it in public. That is a slightly odd state for an open source Django and Wagtail package with podcast feeds, comments, media handling, themes, and enough production use to have opinions.

I started building it in September 2017. Nearly nine years later, it has slowly turned from a personal publishing tool into something other people could plausibly use.

The short version: Django Cast is for people who want to build a blogging or podcasting site, and who are fine with Wagtail as the CMS, Django as the application foundation, and podcasting as part of the content model rather than a bolt-on. Blog-only is also fine — several Django Cast sites have no podcast at all.

Why I Built It

From the start the goal was a blog — a Django-powered place to write and share photos that I owned end to end. I had used Django for years, I wanted something I could understand, extend, deploy, and keep under my own control. Off-the-shelf would have been faster. I built my own anyway. That is roughly how every Django project starts.

The first version used the Django admin as a content interface. Posts were written in Django template syntax, with custom template tags for images, audio, and video. For a technical user that was surprisingly productive. It was also obviously not something I could hand to anyone else: editable template code is not a publishing interface, it is a footgun.

That mattered because I had bigger ambitions early on. Shortly after starting the project I wanted to build a podcast and blog hosting service on top of it. In that kind of system, content needs to be editable by people who do not write HTML for fun.

Wagtail changed the shape of the project. Django stayed as the foundation, but Wagtail added a real CMS: pages, StreamField, image choosers, snippets, permissions, and an editor interface that does not assume the editor is also a frontend developer. Django Cast became less of a personal blog engine and more of a package for building content sites.

The podcast side was there early — python-podcast.de has always run on it, and podcasting is the reason for the "Cast" in the name. There was also a less neutral reason: a podcast about Python had no business running on WordPress. Over time the package accumulated the things a podcast site needs: episodes, audio enclosures, podcast feeds, artwork, chapters, transcript uploads, transcript generation from inside Wagtail, and the assorted podcast-specific metadata that podcast clients quietly demand. The long-term goal is something comparable to the Podlove Publisher plugin for WordPress, but for Django and Wagtail.

What Django Cast Is Today

Django Cast is a Django package that uses Wagtail to make content editable. Its models cover blogs, posts, podcasts, and episodes. A Podcast is a Blog. An Episode is a Post.

That sounds banal, but it has consequences: podcasting is part of the same content system as the rest of the site. Episodes use the same editing tools as posts. They get StreamField content, images, galleries, embeds, comments, tags, categories, search, and faceted filtering for free. Podcast feeds are basically blog feeds plus an audio enclosure and a few iTunes attributes — most of the feed machinery is shared.

A fair amount of machinery has grown on top of that core:

  • Wagtail-managed blogs, posts, podcasts, and episodes, all sharing the same content tools
  • RSS, Atom, and podcast feeds with audio enclosures and podcast metadata
  • audio in multiple formats (m4a, mp3, oga, opus) with chapter marks, transcripts (uploaded or generated inside Wagtail), and per-episode artwork
  • responsive AVIF images, galleries, and reusable cover images that work across pages, feeds, players, and previews
  • tags, categories, search, and faceted navigation
  • a built-in comment system with a Naive Bayes spam filter, AJAX posting without login, and no third-party services
  • swappable themes, including full SPA frontends via a REST API
  • enough performance work that feeds and index pages don't melt under real archives
  • mostly type-annotated code and 100% test coverage (the fail_under = 100 line in pyproject.toml is not aspirational, it is enforced)

Django Cast is at its best when the goal is not "put an audio file on a webpage" but "build a Django/Wagtail site where podcast publishing is part of the domain model."

Keeping Feeds From Melting

The most significant internal change in Django Cast was driven by performance — but the performance work is only half the story. The other half is being honest about where content data is collected, how it is shaped, and how it gets handed to templates, feeds, and APIs.

Content sites have a habit of growing slowly until performance problems become structural. A blog index page is not really a list of posts. It is cover images, galleries, tags, categories, responsive image renditions, comments, chapter marks, and pagination. A podcast feed is rarely a handful of items; it is the whole archive, with audio metadata, artwork, chapters, and transcript references attached to each one. Resolve all of that casually from templates and the database starts crying.

Django Cast addresses this with a repository / read-model layer between the database and rendering. The page or feed first collects what it needs in an explicit query phase. That data is shaped into serializable structures and then handed to rendering. In practice this means the HTML can be rendered from a plain dictionary, with no further database access. Which means the expensive part is cacheable.

It also means a slow page has one place to look. The query phase is the only spot where the database can hurt you; everything downstream is deterministic, and that turns "the site feels slow today" into a question with an actual answer.

This matters most for feeds. Feed readers and podcast clients hammer feed endpoints, and feed endpoints often contain many items. If feed rendering does a large number of accidental queries, the site pays for that mistake all day, every day.

The result is a system that can serve archives with hundreds of posts and episodes without relying on hope. It is still Django and Wagtail. The expensive parts are just made explicit enough to optimize, cache, and reason about.

Media Is Not An Attachment

Responsive images were one of the original reasons Django Cast existed. Modern publishing is image-heavy, and image handling should not be an afterthought you tape on later. The hard part is not loading an image or playing an audio file. It is making the same upload behave correctly across page rendering, feed enclosures, players, galleries, and social previews.

Django Cast builds on Wagtail's image system and adds project-specific handling for responsive renditions, galleries, and media blocks. AVIF is the primary format, with responsive renditions so browsers can get an image in an appropriate size and format. Galleries render as interactive frontend components.

Audio is the other major media type. Django Cast handles audio files in multiple formats, generates podcast feed enclosures, exposes episode metadata, and integrates with the Podlove Web Player for browser playback. It also supports chapters, transcript uploads, and transcript generation from inside Wagtail, so clients and pages can expose more than just a play button.

The audio files themselves are usually served from a CDN backed by an S3-compatible object store. Serving them directly from Django with async views works too — I was not brave enough to try it yet, but it should work.

Video support exists, but is less central and less polished. Honest version: nobody has asked me for more, so it has stayed where it is.

Comments, Without The Hostage Situation

Comments are part of Django Cast, not a third-party widget. That decision was deliberate. Disqus and friends solve the spam problem by handing your readers, their identities, and their data to someone else. I would rather solve it in-process.

So Django Cast ships its own comment system, built on django-contrib-comments, with a Naive Bayes spam classifier as the default moderator. Readers can post comments via AJAX without logging in.

There is no third-party JavaScript, no external account, no tracking pixel, no "please sign in to comment" page. Comments can be enabled or disabled at the app, blog, or post level, so silencing a thread or a whole blog is one field flip.

The spam filter is what makes the rest of it workable. On python-podcast.de it has been running for several years; after some initial training it catches essentially everything, with very little ongoing attention.

Frontends are Replaceable

Themes in Django Cast are not just templates. A theme can replace Django templates surface by surface, or it can skip Django templates entirely and ship a full SPA against the REST API. The bundled Bootstrap 4 theme and the sibling Bootstrap 5 and Vue themes show the range — Bootstrap server-rendered, Vue speaking only to the API.

The Bootstrap-based themes are useful today, but Bootstrap 5 is not where I want to spend the future. The more interesting direction is plain templates based on Every Layout ideas, styled properly, without Bootstrap underneath. Tailwind-based themes should also be straightforward.

The distinction matters: Django Cast is not trying to impose one fixed frontend. It is a Django package — content model, editor workflows, feeds, media handling, extension points — and you build the site on top.

The Unfinished Parts

The defaults are the most visible gap. The theming system exists and the project can be made to look good, but the out-of-the-box experience does not yet show what Django Cast is capable of. The plan is a default theme based on Every Layout, with Tailwind and other alternatives as siblings, and Bootstrap 5 quietly stepping back from the long-term direction.

The audio player deserves another look. Podlove Web Player has useful features and is well known in the podcasting world. It also brings a large JavaScript payload and some complexity, especially when several players end up on one page. Some themes lazy-load it; that is a workaround, not a fix. Longer term I want to replace it with something lighter — either Able Player or a homegrown player built from HTML, CSS, and web components.

Documentation is the other ongoing area. It covers installation, content models, feeds, themes, media, deployment, and operations. Some of it is genuinely useful. Some of it still reflects a project that grew organically over years. Better docs are not optional if the project is going to be usable by people who were not involved in building it.

The current feature in flight is episode contributors. Podcast episodes usually involve more than one page owner: hosts, co-hosts, guests, and other people who should be credited. Django Cast is gaining reusable contributor snippets and episode-level contributor assignments, so episode pages and podcast feeds can credit the right people, with Podcasting 2.0 podcast:person metadata where appropriate. Small in scope, but representative of the direction: podcast concepts should be visible in the database, the Wagtail editor, the templates, and the feeds — not just one of those.

The rest of the roadmap groups into three themes:

  • Presentation: move the default toward Every Layout, make Tailwind-based themes easy, replace the third-party player.
  • Podcasts and feeds: paged feeds, better contributor and Podcasting 2.0 support, proper typeahead search.
  • Adoption: clearer installation, deployment, and migration docs, plus real sites used to find the rough edges in editorial workflows.

And the project is still pre-1.0. It has real production history. It also has interfaces and defaults I want to improve before promising they will not move.

The Shape Of The Project

Django Cast is a Django-native publishing system for people who want to own their site, their feed, and their content model. Wagtail handles editing, Django handles the application, templates or APIs handle the frontend. It is most interesting for podcasts that want more control than a hosted platform provides, without giving up a real editorial interface.

The hosted-platform idea is still in the background. Not a launch promise, more of a compass: non-technical editing, structured podcast metadata, feed correctness, media handling, and themeable frontends all matter the moment the package has to serve more than one site.

The project has spent years proving it can run real sites. The next job is making that obvious before someone has to read the source.