Skip to content
View in the app

A better way to browse. Learn more.

ernestdefoe.online

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
ernestdefoe.online

Extensions, themes & support for Flarum and Invision Community

Vibe coding for the community web. Report a bug, request a feature, or dig into the source — this is where the tools you use get built, in the open.

We do custom Bespoke Invision Community apps. If you have an idea for something you want then use the contact form to get in touch with us.
Knowledge base

Things that cost me a day, so they cost you none

Working notes from building Invision Community and Flarum applications. Mostly the failures that give no error at all — the ones where everything installs cleanly and quietly does the wrong thing.

92 articles

Invision Community 5

86 articles

Extensions and contracts

39

What each extension point is for, what it must declare, and what happens when it is wrong — which is usually nothing visible.

Languages and text

5

The string table, translation, and the places where text does not appear where you expected it to.

Theming, templates and forms

9

Theme hooks, CSS that survives both colour schemes, and building forms that do not throw on render.

Background work and scheduled tasks

5

The queue system, work that has to happen after the response, and jobs that finish without doing anything.

Data, settings and storage

11

The database layer, settings, tags, file storage, and backing up a live site.

AI features and expectations

5

What these features do, what they cost, and what buyers reasonably but wrongly assume they do.

Application structure and releases

11

The JSON files an application is made of, versioning and upgrade steps, and testing from the command line.

Realtime, chat and calls

1

WebSocket gateways, relays and the server-side pieces live features depend on — where "it works when I test it" and "it works for your members" are different claims.

Nothing matches that.

Building a podcast feed Apple will accept

If you are adding podcast episodes to a community, the temptation is a Pages database with an audio upload field and a player on the template. That gets you a web page. It does not get you a podcast, because a podcast is the RSS feed, and directories are unforgiving about details that produce no error anywhere.

These are the rules worth knowing before you write the feed, each one a documented rejection or a player misbehaving.

The namespaces, spelled exactly

<rss version="2.0"
     xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:atom="http://www.w3.org/2005/Atom">

The itunes one is a http:// DTD URL. Validators match the string, not the resource, so "correcting" it to https silently invalidates every itunes: tag in the file.

explicit is the string "true" or "false"

It was yes/no years ago and a great many tutorials still say so. The current spec rejects those.

<itunes:explicit>false</itunes:explicit>   <!-- correct -->
<itunes:explicit>no</itunes:explicit>      <!-- fails validation -->

image and category carry their value as an ATTRIBUTE

As element text they are silently ignored, and your show appears with no artwork and no category. This is the most common complaint about hand-written feeds.

<itunes:image href="https://example.com/art.jpg"/>
<itunes:category text="Society &amp; Culture">
    <itunes:category text="Documentary"/>
</itunes:category>

Artwork must also be square and between 1400×1400 and 3000×3000. Validate it on upload — a rejection weeks later, from Apple, is a much worse way to find out.

enclosure length is in BYTES, and must be the real number

<enclosure url="https://example.com/ep1.mp3" length="10856403" type="audio/mpeg"/>

Players size the scrub bar from this and use it to decide the download finished. A zero or a guess produces audio that cannot be seeked and sometimes stops early. Read it from the stored file rather than trusting anything a form told you:

$file  = \IPS\File::get( 'myapp_Audio', $stored );
$bytes = (int) $file->filesize();

Get the MIME type right too — an m4a announced as audio/mpeg is refused by some players before it downloads anything.

The GUID must be permanent and opaque

This is the one that costs you subscribers. Players use the GUID to decide what they already have.

<guid isPermaLink="false">a3f9c1e07b42...</guid>

Derive it from the title, the URL or the row id — all of which look tidier — and the first time an episode is renamed or the site moves, every subscriber's app treats the whole back catalogue as new, re-downloads it, and sends a push notification per episode. Generate a random value once, store it, and never touch it again:

$guid = bin2hex( random_bytes( 16 ) );   /* stored on the row, forever */

pubDate must be RFC 2822

$x->writeElement( 'pubDate', date( \DateTimeInterface::RFC2822, $published ) );

ISO 8601 is accepted by some apps and silently dropped by others, so an episode with the wrong format appears undated or not at all.

episodic versus serial changes how apps ORDER your show

<itunes:type>serial</itunes:type>

Serial tells apps to present episode 1 first. A serial show marked episodic is listed newest-first, so a new listener starts at the ending. Apple also requires itunes:episode numbers on serial shows.

Exclude drafts and future episodes in the QUERY

Not in a filter afterwards, and not in the template:

Db::i()->select( '*', 'myapp_episodes', array(
    "show_id=? AND state='published' AND published<=? AND audio<>''", $showId, time()
), 'published DESC', 300 );

An episode that reaches the feed a minute early is an episode every subscriber has already downloaded. There is no recall. The same check has to guard the episode's own web page — its URL is guessable from the previous one.

The 300 limit is deliberate: Apple reads roughly that many, and a five-year weekly show returning everything is a megabyte of XML fetched by every podcast app several times a day.

Serving it

Output::i()->sendOutput( $xml, 200, 'application/rss+xml', array(
    'Cache-Control' => 'public, max-age=600',
) );

Three things there. application/rss+xml — served as text/html a podcast app refuses the feed. No charset in that stringsendOutput() appends one, and stating it yourself produces application/rss+xml;charset=UTF-8;charset=UTF-8. And use sendOutput() rather than the normal output path, or the site's HTML wrapper is prepended and the feed is invalid on its first byte.

Test the feed, not the page

Every rule above can be asserted without a directory:

$doc = simplexml_load_string( $xml );
$itunes = $doc->channel->children( 'http://www.itunes.com/dtds/podcast-1.0.dtd' );

assert( (string) $itunes->explicit === 'false' );
assert( (string) $item->enclosure['length'] !== '0' );
assert( \DateTime::createFromFormat( \DateTimeInterface::RFC2822, (string) $item->pubDate ) !== FALSE );

⚠️ One trap in the test rather than the feed: on an element reached through children( $namespace ), SimpleXML returns '' for $el['attr'] even when the attribute is there. You must call ->attributes():

(string) $itunes->category['text']                 // '' - looks like a broken feed
(string) $itunes->category->attributes()['text']   // 'Technology'

That cost two false failures against a feed that was entirely correct. Read the raw XML before you "fix" anything a test complains about.

Finally, the feed address is permanent

Once anybody has subscribed, every podcast app remembers the URL. Give it a friendly one from the start — /podcasts/feed/12-my-show rather than index.php?app=…&controller=feed&id=12 — because you cannot tidy it up later without stranding every subscriber you have.



User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.