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.

Running code on every request, after the page has been sent

Some work has to happen on every page view — recording an analytic, touching a cache, noting that something was seen — and none of it should make the visitor wait. Invision Community supports this, but the pieces are not obvious and the method that looks right is the wrong one.

core/Loader is the only per-request extension point

Dispatcher\Front invokes exactly one extension point on every front-end request, and it invokes it three times:

// early, while building the page
foreach ( Application::allExtensions( 'core', 'Loader' ) as $loader )
{
    foreach ( $loader->js() as $js )   { ... }
    foreach ( $loader->css() as $css ) { ... }
}

// before dispatch, first one wins
foreach ( Application::allExtensions( 'core', 'Loader' ) as $loader )
{
    if ( $redirect = $loader->checkForRedirect() ) { Output::i()->redirect( $redirect ); }
}

// at the end
foreach ( Application::allExtensions( 'core', 'Loader' ) as $loader )
{
    $loader->onFinish();
}

Despite the name, core/Loader is not only for assets. onFinish() is a general end-of-request hook, and it is the supported place for per-request work.

onFinish() does NOT run after the response

This is the trap. Front::finish() calls onFinish() and then calls parent::finish(), which is what actually sends the page:

foreach ( Application::allExtensions( 'core', 'Loader' ) as $loader )
{
    $loader->onFinish();
}

parent::finish();   // <- Output::sendOutput() happens in here

So anything done directly inside onFinish() delays the response. A database write there is time the visitor spends looking at a blank tab.

The part that makes it work

Output::sendOutput() ends like this:

/* Flush and exit */
@ob_end_flush();
@flush();

/* If using PHP-FPM, close the request so that __destruct tasks are run after
    data is flushed to the browser */
if( function_exists( 'fastcgi_finish_request' ) )
{
    fastcgi_finish_request();
}

exit;

Core deliberately releases the connection and keeps executing — and exit still runs shutdown functions and destructors. So the pattern is:

Gather what you need in onFinish() from data already in memory, and register a shutdown function to do the writing.

public function onFinish(): void
{
    try
    {
        $row = array( /* ... built from Request/Member/$_SERVER, no queries ... */ );

        register_shutdown_function( static function () use ( $row ) {
            try
            {
                \IPS\Db::i()->insert( 'myapp_events', $row );
            }
            catch ( \Throwable $e )
            {
                /* the visitor already has their page */
            }
        } );
    }
    catch ( \Throwable $e )
    {
        /* never break a page */
    }
}

Under PHP-FPM the write happens after the browser has the page. Without FPM the shutdown function still runs after the output has been flushed, so it is never worse than doing the work inline.

Rules for anything on this hook

  • Do no reads. Use what is already in memory — Request::i(), Member::loggedIn(), $_SERVER. A query here runs on every page on the site.
  • Swallow everything. This code runs during somebody reading a topic. An uncaught exception is their broken page.
  • Guard against running twice with a static flag, in case the hook fires more than once in one request.
  • Skip AJAX if you are counting page views — Request::i()->isAjax() — or every figure you produce will be inflated.
  • One statement. Even after the response, the PHP-FPM worker is still occupied and cannot serve anyone else until the script ends.

🚨 And the one that takes the whole site down

If you register a core/Loader extension you must implement js() and css() correctly even when you have no assets, because the dispatcher merges each returned element:

foreach ( $loader->js() as $js ) { $jsFiles = array_merge( $jsFiles, $js ); }

Each element must itself be an array. Returning array( $url ) throws inside the dispatcher and every front page on the site returns a 500 — not just yours, and with nothing in the trace naming the application responsible.

public function js(): array  { return array(); }               // safe: no assets
public function css(): array { return array( array( $url ) ); } // an array OF arrays

An empty array is safe, because the loop simply does not run.

Verified against

Invision Community 5.0.19, by reading system/Dispatcher/Front.php, system/Dispatcher/Dispatcher.php, system/Output/Output.php and system/Extensions/LoaderAbstract.php.



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.