Building the Loader Addon

Why a separate addon

A theme folder cannot execute PHP. The logic needed to inject CSS — checking the active theme, respecting the ignore_theme flag, serving the file — requires PHP. A companion loader addon handles all of this. The theme and the loader are distributed together as two separate zips.

Folder layout

addons/my-theme-loader/
├── addon.json
└── addon.php

addon.json

{
  "slug": "my-theme-loader",
  "name": "My Theme Loader",
  "version": "1.0.0",
  "description": "Injects the My Theme stylesheet. Requires the My Theme theme to be installed and active."
}

addon.php

Hooks::on('wordcore_loaded', function () {

    $settings        = Storage::get('core/settings', []);
    $activeTheme     = $settings['active_theme'] ?? '';
    $installedThemes = Storage::get('core/themes', []);
    $themeInstalled  = isset($installedThemes['my-theme']);
    $themeActive     = $activeTheme === 'my-theme';

    if ($themeActive) {

        // Respect the Pages addon ignore_theme flag
        $requestUri   = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
        $base         = WordCore::base();
        $relativePath = $base
            ? preg_replace('#^' . preg_quote($base, '#') . '#', '', $requestUri)
            : $requestUri;
        $relativePath = '/' . ltrim($relativePath, '/');

        $ignoreTheme = false;
        foreach (Storage::get('pages/pages', []) as $page) {
            $slug = '/' . ltrim($page['slug'] ?? '', '/');
            if ($slug === $relativePath && !empty($page['ignore_theme'])) {
                $ignoreTheme = true;
                break;
            }
        }

        if (!$ignoreTheme) {
            Hooks::on('head', function () {
                $file = WC_ROOT . '/themes/my-theme/style.css';
                if (file_exists($file)) {
                    echo '<style>' . file_get_contents($file) . '</style>';
                }
            });
        }

    } else {
        // Warn on the front end if theme is missing or inactive
        $msg = !$themeInstalled
            ? 'My Theme Loader: the My Theme theme is not installed. Install it or remove this addon.'
            : 'My Theme Loader: the My Theme theme is not active. Activate it or remove this addon.';

        Hooks::on('head', function () use ($msg) {
            echo '<script>
                document.addEventListener("DOMContentLoaded", function () {
                    var b = document.createElement("div");
                    b.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:99999;background:#c44240;color:#fff;font-family:Arial,sans-serif;font-size:13px;font-weight:600;padding:10px 16px;text-align:center;";
                    b.textContent = ' . json_encode($msg) . ';
                    document.body.prepend(b);
                });
            </script>';
        });
    }

});

Why inline the CSS rather than linking it

The themes/ directory is not publicly accessible — a direct <link> tag pointing into it will return a 403. Routing the file through a public WordCore route works, but causes a flash of unstyled content because the browser has to make a second HTTP request after the page has already rendered. Reading the file with file_get_contents() and echoing it as an inline <style> block delivers the CSS in the initial HTML response with no extra requests and no flash.