Flexa Cache

WordPress plugin · v1.0.0

Flexa Cache

Full-stack WordPress page cache and front-end optimizer. Serves static HTML before WordPress boots via an advanced-cache.php drop-in, and minifies, combines and defers CSS/JS assets - managed from a React admin.

Requires WordPress 5.9+Requires PHP 8.2GPL v2138 i18n strings

Introduction

Overview

Flexa Cache intercepts every cacheable WordPress request and serves it from a pre-generated static HTML file, bypassing PHP and the database entirely. Optimization passes - HTML minify, CSS/JS combine, lazy load, defer - run on the buffer before the file is written, so every subsequent request gets the optimized version at zero per-request cost.

The admin - Settings, Cache stats, Purge controls, Preloader - is a single-page React app (Vite + TypeScript + Tailwind v4) talking to a namespaced REST API.

PropertyValue
Current version1.0.0
Requires WordPress5.9 or newer
Requires PHP8.2 or newer
LicenseGPL v2 or later
Text domainflexa-cache
REST namespaceflexa-cache/v1
Cache directorywp-content/cache/flexa/
Option keyflexa_cache_settings

Architecture

How it works

Three layers stack on top of each other, each acting before the previous one would have done its work.

LayerWhen it runsResponsibility
advanced-cache.phpBefore WordPress loadsSelf-contained raw PHP (no autoload, no WP functions). Resolves the request URL to a path under wp-content/cache/flexa/, serves index.html (or index-mobile.html for mobile-split) directly if it exists, otherwise falls through to WordPress.
Cache\Engineinit + shutdown (WP request)Cacheability gate: skips admin, AJAX, REST, cron, feed, preview, logged-in users, cookie signals, excluded URLs. On cacheable requests starts ob_start(), finalizes the buffer at shutdown: runs optimization passes then writes the file via CacheStore.
Optimize\*flexa_cache/engine/buffer hookMinify (pri 99), LazyLoad (pri 10), Assets combine (pri 5), GoogleFonts (pri 8) all attach to the buffer action and transform the HTML in order. RenderBlocking and DelayJs attach to script_loader_tag and run on every response (cached or not).

The request-to-path algorithm in the drop-in mirrors Cache\Store::relative_path() byte-for-byte: lowercase host + sanitized URL path + trailing /index.html. Changing one requires changing both or cache hits silently miss.

Drop-in serving logic (simplified)
// Before WordPress loads — no WP functions available.
if (!defined('WP_CACHE') || !WP_CACHE) return;

$path = build_cache_path($_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']);

// Mobile-split: a 0-byte sentinel written by Engine::finalize()
// when the mobile_theme setting is on.
if (is_mobile_ua() && is_readable($path . '/.mobile-split')) {
    $file = $path . '/index-mobile.html';
} else {
    $file = $path . '/index.html';
}

if (is_readable($file)) {
    // Serve the static file and exit before WordPress boots.
    header('Content-Type: text/html; charset=UTF-8');
    readfile($file);
    exit;
}
// Fall through to WordPress.

Getting started

Installation

  1. Upload the flexa-cache folder to wp-content/plugins/ (or install the .zip from the Plugins screen).
  2. Activate the plugin through the WordPress "Plugins" menu.
  3. On activation the plugin copies advanced-cache.php to wp-content/advanced-cache.php and adds define('WP_CACHE', true); to wp-config.php (idempotent - safe to run multiple times).
  4. Open Flexa Cache and flip on Enable caching.

Foreign drop-in protection

Flexa Cache will never overwrite an advanced-cache.php that was created by another plugin. If you see a notice saying the drop-in was skipped, check whether another caching plugin (W3 Total Cache, WP Super Cache, etc.) is active and either deactivate it or remove its drop-in first.

Capabilities

Features

FeatureSetting keyWhat it does
Page cacheenabledMaster switch. Installs / removes the drop-in and WP_CACHE define.
Exclude logged-inexclude_logged_inBypass cache for authenticated users.
Exclude mobileexclude_mobileBypass cache for mobile user agents.
Clear on new postclear_on_new_postFull purge on publish_post.
Clear on updateclear_on_update_postFull purge on post_updated.
GzipgzipPre-compressed .html.gz siblings + mod_deflate .htaccess block.
Minify HTMLminify_htmlWhitespace collapse on the cached buffer.
Minify HTML Plusminify_html_plusAdds safe inline-CSS minify and whitespace-only inline-JS trim.
Browser cachingbrowser_cachingCache-Control / Expires .htaccess block for static assets.
Disable emojisdisable_emojisDequeues the WP emoji detection script and related assets.
Lazy loadlazy_loadInjects loading="lazy" decoding="async" on <img> tags.
Minify CSSminify_cssStrips whitespace/comments from CSS before combining.
Minify CSS Plusminify_css_plusAdds punctuation-level CSS minification.
Combine CSScombine_cssFuses consecutive same-region <link> tags into one bundle.
Combine JScombine_jsFuses consecutive same-region <script src> tags into one bundle.
Combine JS Pluscombine_js_plusMirrors Combine JS for the secondary JS combine pass.
Render blocking JSrender_blocking_jsAdds defer to external <script> tags via script_loader_tag.
Google Fonts asyncgoogle_fonts_asyncRewrites fonts.googleapis.com <link> to preload+onload swap.
Delay JSdelay_jsNeutralizes external scripts to type="flexa-delay", re-injects on first interaction or 7s idle.
PreloadpreloadAuto-warm the cache after a full purge via Action Scheduler / WP-Cron.
Widget cachewidget_cacheCaches classic WP_Widget output in versioned transients.
Mobile thememobile_themeWrites a .mobile-split sentinel; mobile UAs get index-mobile.html.

minify_js is a safe no-op

There is no standalone minify_js toggle because Flexa Cache ships no JavaScript parser. JavaScript is only ever concatenated (via combine_js), never minified. A standalone minify_js switch would be misleading.

Configuration

Settings reference

All settings are stored as a single serialized array under the WordPress option key flexa_cache_settings. The REST API uses partial-diff saves: only changed keys are sent; the PHP Settings::sanitize() method merges them over the stored values.

KeyTypeDefault
enabledboolfalse
exclude_logged_inbooltrue
exclude_mobileboolfalse
clear_on_new_postbooltrue
clear_on_update_postbooltrue
gzipboolfalse
minify_htmlboolfalse
minify_html_plusboolfalse
browser_cachingboolfalse
disable_emojisboolfalse
lazy_loadboolfalse
minify_cssboolfalse
minify_css_plusboolfalse
combine_cssboolfalse
combine_jsboolfalse
combine_js_plusboolfalse
render_blocking_jsboolfalse
google_fonts_asyncboolfalse
delay_jsboolfalse
preloadboolfalse
widget_cacheboolfalse
mobile_themeboolfalse
languagestringen
exclusions.urlsstring[][]
exclusions.cssstring[][]
exclusions.jsstring[][]

Partial-diff saves

The settings UI sends only the keys that changed in the current edit session. Sending the whole blob would let a single-toggle save clobber a concurrent change made in another tab. The PHP merge-over-stored pattern prevents that race.

Developer reference

REST API

All routes are registered under /wp-json/flexa-cache/v1/. Every route requires current_user_can('manage_options') and standard WordPress REST authentication (auth cookie + X-WP-Nonce from wp_create_nonce('wp_rest')).

MethodRoutePurpose
GET/cache/statsReturns { cached_pages, cache_size, last_cleared }.
POST/cache/purgePurge cache. Body: {"scope":"all"} or {"scope":"url","url":"https://..."}. Returns the updated stats object.
GET/cache/preloadReturns preloader status: { active, queued, completed, total }.
POST/cache/preloadControl the preloader. Body: {"action":"start"} or {"action":"cancel"}.
GET POST/settingsGET returns the full settings object. POST accepts a partial object and merges it over the stored values.
Example: purge a single URL
fetch('/wp-json/flexa-cache/v1/cache/purge', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': wpApiSettings.nonce,
  },
  body: JSON.stringify({
    scope: 'url',
    url: 'https://example.com/blog/my-post/',
  }),
})
  .then(r => r.json())
  .then(console.log);

Extensibility

Hooks & filters

Flexa Cache exposes several slash-separated action and filter hooks so integrators can extend or gate behavior without modifying plugin files.

HookTypeSignature / notes
flexa_cache/engine/is_cacheablefilterbool $cacheable — override the cacheability decision for the current request.
flexa_cache/engine/bufferactionstring &$buffer — the full HTML output before it is written to disk. Optimization passes attach here.
flexa_cache/settings/updatedactionarray $new, array $old — fired after settings are saved. Gzip and BrowserCache .htaccess writes attach here.
flexa_cache/purgedactionstring $scope, string $url — fired after a cache purge. The Preloader auto-warm attaches here.
flexa_cache/data_resetaction(no args) — fired by Resetter::reset_all(). .htaccess blocks, preloader state, and widget cache version are cleared here.
flexa_cache/optimize/should_optimizefilterbool $should — override the OptimizeGuard decision for the current request.
flexa_cache/widget_cache/skipfilterbool $skip, WP_Widget $widget — return true to bypass widget caching for a specific widget instance.
flexa_cache/preloader/urlsfilterstring[] $urls — the list of URLs the preloader will crawl. Add, remove or reorder entries.
Example: mark a request as non-cacheable
add_filter('flexa_cache/engine/is_cacheable', function (bool $cacheable): bool {
    // Never cache the /live-scores/ subtree.
    if (str_starts_with($_SERVER['REQUEST_URI'], '/live-scores/')) {
        return false;
    }
    return $cacheable;
});

Command line

WP-CLI

Flexa Cache registers commands under the wp flexa-cache namespace. All commands route through the same shared PHP paths as the REST API and admin UI.

CommandDescription
wp flexa-cache clearPurge the entire cache.
wp flexa-cache clear --url=<url>Purge the cached file for a single URL.
wp flexa-cache statusPrint cached pages, cache size and last-cleared timestamp.
wp flexa-cache preload startQueue and start the cache preloader.
wp flexa-cache preload statusPrint preloader state: active, queued, completed, total.
wp flexa-cache preload cancelCancel a running preload job.
Example: clear and immediately start preloading
wp flexa-cache clear && wp flexa-cache preload start

Localization

Internationalization

Text domain flexa-cache. Both PHP strings (.mo) and the React admin JS strings (script-translation .json via wp_set_script_translations) are localisable. The catalog contains 138 strings (136 JS + 12 PHP).

Because wp i18n make-pot cannot parse .tsx/.ts files directly, the plugin ships scripts/extract-tsx-i18n.mjs - a Node script that emits a transient JS shim of every __(), _x() and _n() call. The shim is passed to make-pot and deleted afterwards. The full flow:

Regenerating the .pot catalog
bash makepot.sh

Static i18n literals only

The makepot script is a static extractor. Translation function calls must use literal strings, not variables or template literals, or the catalog will be incomplete.

History

Changelog

v1.0.0

  • Initial release. Full-stack WordPress page cache: drop-in (advanced-cache.php), cache engine, purge, REST API, WP-CLI, and React admin.
  • Optimize: HTML minify, Gzip, Browser caching, Disable emojis, Lazy load.
  • Assets: CSS minify + combine, JS combine, Render-blocking JS defer, Google Fonts async, Delay JS.
  • Phase 4: Cache preloader (Action Scheduler / WP-Cron), widget cache (versioned transients), mobile-theme split cache (.mobile-split sentinel), full WP-CLI preload commands, i18n/makepot (138 strings).
  • Security: All REST routesmanage_options-gated, output escaped throughout, ABSPATH guard on every shipped file, zero $wpdb, no dangerous functions.