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.
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.
| Property | Value |
|---|---|
| Current version | 1.0.0 |
| Requires WordPress | 5.9 or newer |
| Requires PHP | 8.2 or newer |
| License | GPL v2 or later |
| Text domain | flexa-cache |
| REST namespace | flexa-cache/v1 |
| Cache directory | wp-content/cache/flexa/ |
| Option key | flexa_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.
| Layer | When it runs | Responsibility |
|---|---|---|
advanced-cache.php | Before WordPress loads | Self-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\Engine | init + 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 hook | Minify (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.
// 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
- Upload the
flexa-cachefolder towp-content/plugins/(or install the.zipfrom the Plugins screen). - Activate the plugin through the WordPress "Plugins" menu.
- On activation the plugin copies
advanced-cache.phptowp-content/advanced-cache.phpand addsdefine('WP_CACHE', true);towp-config.php(idempotent - safe to run multiple times). - 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
| Feature | Setting key | What it does |
|---|---|---|
| Page cache | enabled | Master switch. Installs / removes the drop-in and WP_CACHE define. |
| Exclude logged-in | exclude_logged_in | Bypass cache for authenticated users. |
| Exclude mobile | exclude_mobile | Bypass cache for mobile user agents. |
| Clear on new post | clear_on_new_post | Full purge on publish_post. |
| Clear on update | clear_on_update_post | Full purge on post_updated. |
| Gzip | gzip | Pre-compressed .html.gz siblings + mod_deflate .htaccess block. |
| Minify HTML | minify_html | Whitespace collapse on the cached buffer. |
| Minify HTML Plus | minify_html_plus | Adds safe inline-CSS minify and whitespace-only inline-JS trim. |
| Browser caching | browser_caching | Cache-Control / Expires .htaccess block for static assets. |
| Disable emojis | disable_emojis | Dequeues the WP emoji detection script and related assets. |
| Lazy load | lazy_load | Injects loading="lazy" decoding="async" on <img> tags. |
| Minify CSS | minify_css | Strips whitespace/comments from CSS before combining. |
| Minify CSS Plus | minify_css_plus | Adds punctuation-level CSS minification. |
| Combine CSS | combine_css | Fuses consecutive same-region <link> tags into one bundle. |
| Combine JS | combine_js | Fuses consecutive same-region <script src> tags into one bundle. |
| Combine JS Plus | combine_js_plus | Mirrors Combine JS for the secondary JS combine pass. |
| Render blocking JS | render_blocking_js | Adds defer to external <script> tags via script_loader_tag. |
| Google Fonts async | google_fonts_async | Rewrites fonts.googleapis.com <link> to preload+onload swap. |
| Delay JS | delay_js | Neutralizes external scripts to type="flexa-delay", re-injects on first interaction or 7s idle. |
| Preload | preload | Auto-warm the cache after a full purge via Action Scheduler / WP-Cron. |
| Widget cache | widget_cache | Caches classic WP_Widget output in versioned transients. |
| Mobile theme | mobile_theme | Writes 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.
| Key | Type | Default |
|---|---|---|
enabled | bool | false |
exclude_logged_in | bool | true |
exclude_mobile | bool | false |
clear_on_new_post | bool | true |
clear_on_update_post | bool | true |
gzip | bool | false |
minify_html | bool | false |
minify_html_plus | bool | false |
browser_caching | bool | false |
disable_emojis | bool | false |
lazy_load | bool | false |
minify_css | bool | false |
minify_css_plus | bool | false |
combine_css | bool | false |
combine_js | bool | false |
combine_js_plus | bool | false |
render_blocking_js | bool | false |
google_fonts_async | bool | false |
delay_js | bool | false |
preload | bool | false |
widget_cache | bool | false |
mobile_theme | bool | false |
language | string | en |
exclusions.urls | string[] | [] |
exclusions.css | string[] | [] |
exclusions.js | string[] | [] |
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')).
| Method | Route | Purpose |
|---|---|---|
| GET | /cache/stats | Returns { cached_pages, cache_size, last_cleared }. |
| POST | /cache/purge | Purge cache. Body: {"scope":"all"} or {"scope":"url","url":"https://..."}. Returns the updated stats object. |
| GET | /cache/preload | Returns preloader status: { active, queued, completed, total }. |
| POST | /cache/preload | Control the preloader. Body: {"action":"start"} or {"action":"cancel"}. |
| GET POST | /settings | GET returns the full settings object. POST accepts a partial object and merges it over the stored values. |
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.
| Hook | Type | Signature / notes |
|---|---|---|
flexa_cache/engine/is_cacheable | filter | bool $cacheable — override the cacheability decision for the current request. |
flexa_cache/engine/buffer | action | string &$buffer — the full HTML output before it is written to disk. Optimization passes attach here. |
flexa_cache/settings/updated | action | array $new, array $old — fired after settings are saved. Gzip and BrowserCache .htaccess writes attach here. |
flexa_cache/purged | action | string $scope, string $url — fired after a cache purge. The Preloader auto-warm attaches here. |
flexa_cache/data_reset | action | (no args) — fired by Resetter::reset_all(). .htaccess blocks, preloader state, and widget cache version are cleared here. |
flexa_cache/optimize/should_optimize | filter | bool $should — override the OptimizeGuard decision for the current request. |
flexa_cache/widget_cache/skip | filter | bool $skip, WP_Widget $widget — return true to bypass widget caching for a specific widget instance. |
flexa_cache/preloader/urls | filter | string[] $urls — the list of URLs the preloader will crawl. Add, remove or reorder entries. |
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.
| Command | Description |
|---|---|
wp flexa-cache clear | Purge the entire cache. |
wp flexa-cache clear --url=<url> | Purge the cached file for a single URL. |
wp flexa-cache status | Print cached pages, cache size and last-cleared timestamp. |
wp flexa-cache preload start | Queue and start the cache preloader. |
wp flexa-cache preload status | Print preloader state: active, queued, completed, total. |
wp flexa-cache preload cancel | Cancel a running preload job. |
wp flexa-cache clear && wp flexa-cache preload startLocalization
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:
bash makepot.shStatic 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-splitsentinel), full WP-CLI preload commands, i18n/makepot (138 strings). - Security: All REST routes
manage_options-gated, output escaped throughout, ABSPATH guard on every shipped file, zero$wpdb, no dangerous functions.