WebProCMS is built to be as fast as a flat-file site, as flexible as a database-driven CMS, and smarter than either of them. Most CMSes force you to choose: flat HTML for raw speed, or a database with hundreds of queries per page for flexibility. WebProCMS gives you both, by layering independent caches on top of a flat-file baseline so dynamic features cost nothing extra on the hot path.
The baseline: real files, real routes
Every public page is a real .blade.php file on disk, registered in routes/web.php and served by Livewire. There is no "render the page from the database" step. The route list points directly at the file, the file is opcache-resident, and the layout renders without a single DB query needed to know what the page is.
That's the floor. From there, every dynamic feature is layered on as its own independently-invalidated cache — so editing one shortcode doesn't rebuild thousands of pages, and adding a feature doesn't slow down pages that don't use it.
Tier 1 — Per-page content sidecars
When an editor saves text, images, toggles, or any other field in the page editor, the values are stored in content_overrides (the source of truth). Immediately after the save, WebProCMS compiles every override for that page into a small PHP file at storage/framework/page-data/pages/{slug}.php.
On every public request, PagePreload middleware requires that sidecar before the page renders. Because it's plain PHP, it's opcache-resident — reading saved content costs literally zero database queries once the sidecar exists.
If a sidecar is missing (fresh install, snapshot restore, manual DB edit), the system compiles it on demand and writes it. You never have to "rebuild the cache" manually. Snapshot restores, edits made outside the editor, and load-balanced fleets all heal themselves on first hit.
The editor preview iframe gets its own sidecar at storage/framework/page-data/preview/{token}.php — written on every preview refresh with the editor's in-progress unsaved edits merged on top of saved overrides. The iframe reads from that sidecar via the same PagePreload path the public site uses, so what you see in the preview is exactly how the production renderer will resolve the same content once you save. No separate preview pipeline, no in-memory baking layer.
Tier 2 — Shortcodes (business data) cached separately
Shortcodes are inline tokens like [[phone]] or [[business_name]] that resolve to whatever the user has set in their dashboard. They're the typical "I need to update my phone number in one place and have it appear on 80 pages" feature.
In WordPress and most flat-file generators, updating one shortcode either (a) re-renders every page that contains it, or (b) costs an extra DB query per shortcode per request.
WebProCMS does neither. Shortcodes are loaded via Shortcode::activeByTagCached() — a single Cache::rememberForever() lookup that's invalidated only when a shortcode is saved or deleted. Update your phone number once and every page on the site reflects it instantly, with zero extra queries on public page loads and no rebuild step for the thousands of cached page files.
Tier 3 — Code Snippets
Snippets (analytics tags, chat widgets, custom HTML injected into <head> or <body>) follow the same pattern. Snippet::activeCached() and Snippet::forPageCached() return cross-request caches that are populated once and invalidated only when a snippet is edited. Public requests hit zero queries to resolve snippets, even when there are dozens active.
For comparison, in WordPress each plugin that injects a snippet typically adds at least one database query per request — and some add several. WebProCMS adds zero.
Tier 4 — Media metadata (no extra query per image)
Some CMSes add a database query per image on the page to look up the image's dimensions, alt text, and focal point. On a page with 20 images, that's 20 extra queries just to render a layout you already have on disk.
WebProCMS routes every image through MediaItem::metaByPathCached(), a path-keyed cross-request cache that holds width, height, focal coordinates, and the updated_at cache-buster for every uploaded image. The cache is populated on first lookup and invalidated only when a media item is changed.
The result: you get full responsive srcset generation, focal-point-aware cropping, replacement-aware cache busting, and on-demand resized variants — with the same zero-query cost as if the images were hardcoded into a flat file. Bulk-replacing an image updates every page instantly with no rebuild; the metadata cache invalidates and the next request repopulates.
Tier 5 — Glide image variants (generated once, then served without PHP)
When the browser requests /img/v/2026/07/photo.jpg/w824-q85-v1712345678.webp, WebProCMS resizes the image to that width, encodes it, and writes it to disk. Every later request for that URL is answered by the webserver alone — PHP is never started, the framework is never booted, the database is never opened.
That works because the variant's file path is its URL path. The file for the URL above lives at public/img/v/2026/07/photo.jpg/w824-q85-v1712345678.webp. Nothing clever is required to serve it: every web stack's standard Laravel setup already tries real files before handing a request to PHP, so nginx, Apache and OpenLiteSpeed all do this out of the box with no configuration to add.
So a page with fifteen images costs fifteen PHP workers exactly once, on whichever visit first needs each size, and nothing after that.
The filename is the recipe. Width, quality, output format (as the extension) and the image's version are all spelled out in it, which is what makes the rest safe:
- A URL can never go stale. Change an image's quality, or replace the file, and the name changes with it — so browsers, CDNs and server caches all fetch the new one immediately instead of holding an outdated copy. There is no cache to purge.
- A one-year
immutablelifetime is honest, because the bytes behind a given URL genuinely never change. - The extension tells the truth, so the webserver reports the right content type for a file it generated no part of.
- URLs stay signed. Nobody can invent widths to make the server burn CPU and disk generating them. Checking the signature costs nothing on a warm request, because a warm request never reaches PHP to check.
The cache is disposable. Deleting public/img/ is always safe — every variant regenerates the next time it is asked for, and the warmer refills popular pages in the background. It is excluded from backups for the same reason.
Older links keep working. Image URLs from before this scheme still appear in pages a browser or CDN cached earlier; each one is redirected to its new address rather than breaking.
If it ever stops working, the site says so. A webserver misconfiguration can quietly send every image back through PHP — images still appear, they just cost what they used to, which is the kind of fault that hides for months. So the install watches for PHP being asked to serve an image it had already generated, and reports it (img_static) to the Tools page and the fleet dashboard.
Tier 6 — Optional full-page response cache
For pages where the rendered HTML doesn't vary per visitor, WebProCMS ships Spatie's ResponseCache on every public route. Cached pages are served from disk in single-digit milliseconds, bypassing the framework entirely. The cache is automatically invalidated on shortcode, snippet, or media changes, so editors never have to think about it.
Response caching is opt-in per-deployment and guest-only — logged-in users always see fresh content, which is what makes the rest of the architecture matter. The page-data sidecar + shortcode/snippet/media cache stack means even uncached public requests and authenticated requests are fast, because the layers below ResponseCache are already doing zero queries.
This is the part that matters for SaaS apps, e-commerce stores, member portals, and any site that mixes a public marketing surface with logged-in user dashboards. You can't serve a full-page cache to someone whose page contains their own name, their cart, their bookings, or their account balance — but you absolutely can serve those pages out of flat files with cached content layered on top, and that's what WebProCMS is built for.
Partial personalization — caching the shared part of a per-visitor feature
Tier 6 only caches pages whose HTML is identical for everyone. But many "dynamic" features are mostly visitor-independent with a small personalized sliver — and WebProCMS splits them so the big part still rides the response cache instead of forcing the whole page out of it.
Blog comments are the clearest example. The approved comment list and count are identical for every visitor, so they're server-rendered straight into the response-cached page — crawlable, and zero queries on a cache hit. Only the composer (sign-in buttons vs. the compose box, the reply form, the viewer's own still-pending comment) is per-visitor, and it's gated behind a non-secret wpcms_commenter cookie: the cached page ships the signed-out default, and a visitor's browser only calls back to personalize when that cookie says they're logged in. Guests read a fully static cached page and make zero extra requests; a signed-in reader pays exactly one. The post's cache is busted automatically when a comment's approved visibility changes (approve / spam / trash / delete) — a new pending comment doesn't touch it.
The cookie-consent banner uses the same trick: a single uncached /_cookie-consent/init hit resolves per-visitor banner state on top of an HTML body that's cached identically for everyone.
The principle generalizes: don't surrender the full-page cache just because one widget is personalized — cache the shared 95%, and layer the personalized 5% on top with a cookie-gated callback. It's the same philosophy as the tiers above, applied within a single page instead of across the site.
Tier 7 — Custom content types
Custom content types (content_items) get auto-generated index and show pages — which are real .blade.php files registered in the route list, exactly like every other page. The show page loads its record by ID/slug (one query, the only one a record-driven page actually needs), and everything else — layout, content fields, shortcodes, images — flows through the cache tiers above.
So even a fully data-driven page like /blog/some-post typically incurs one query for the record and zero queries for everything else. Compare that to a typical headless CMS where you fetch the post, then fetch the layout, then fetch each block's content, then fetch each image's metadata.
Tier 8 — Class tree-shaking on save
Tailwind generates utility classes by scanning your source files for class names. Most CMSes that emit dynamic classes either ship the entire Tailwind library (hundreds of KB) or use inline style="" attributes (slow and unmaintainable).
Tailwind generates a utility class only if it can see that class in a source file at build time. Saved content, though, lives in the database — and the machine serving a client site has no Node, no Tailwind, and no build step. So an editor typing tracking-wide into a class field is asking for a rule that the shipped stylesheet was never built with.
WebProCMS closes that gap at runtime, in pure PHP. On every public render, RuntimeCssSupplement scans the runtime-written page blades and the saved content_overrides values, tree-shakes the set of classes actually in use, generates the ones the shipped bundle is missing, and writes public/build/runtime-utilities.css — linked immediately after the base bundle. The file is rewritten only when its content changes, and a daily self-heal pass covers anything edited out of band. Editors get pro-grade class controls on a host with no build tooling at all, and the shipped bundle stays small because it only ever carried the classes the templates themselves use.
Saving a page does not rewrite blade files to record class values (that mechanism was retired). Class attributes still present in a blade are frozen defaults from the moment the template was inserted; the live value lives in content_overrides and resolves through the Tier 1 sidecar like every other field. One source of truth at runtime, one supplement to style it, and no page-file churn on save.
Tier 9 — Shared rows as real @include files
Headers, footers, and any other repeated row pattern live in resources/views/shared-rows/ as ordinary blade includes. Pages reference them with @include('shared-rows.header') — a literal PHP include, not a database lookup.
This means:
- Editing a shared row reflects across every page instantly, with no rebuild step.
- Pages don't store a copy of the shared row's content — they reference it.
- The shared row participates in the same caching tiers as everything else (its content_overrides go in its own sidecar, its classes get tree-shaken into the same Tailwind bundle).
Pure static site generators (Hugo, Jekyll, 11ty, Astro in static mode) don't have a runtime include concept at all — editing a shared row means rebuilding every page that uses it. Runtime flat-file CMSes like Statamic and Kirby do support partials, but their static-cache layers still need invalidation when a partial changes. WebProCMS treats shared rows as ordinary PHP includes that participate in the same per-layer invalidation as everything else — so the change is live on the next request without touching the page sidecars.
Tier 10 — Homepage critical CSS (inline first paint)
On the homepage — every site's highest-traffic entry point — the styles needed to paint the header, any announcement bar, and the first two rows are inlined directly into the page <head>, and the main stylesheet is demoted to a non-blocking background download. First paint no longer waits a full network round-trip for the CSS bundle; anything below the fold styles itself the instant the full bundle lands a moment later.
No headless browser is involved (so it works on every host, including node-free shared hosting): the system renders the homepage internally, slices the HTML through the end of the second row, harvests every class the slice uses, and extracts exactly those rules — in original cascade order — from the built Tailwind bundle. The subset is stored at storage/framework/page-data/critical/home.css with a freshness signature (bundle hash + homepage/partial file versions); any homepage edit, header swap, or CMS update regenerates it automatically on the next uncached visit, the same lazy self-healing model as the content sidecars.
Every failure path — oversized subset, missing bundle, generation error — silently falls back to the normal render-blocking stylesheet, so the feature can only ever help. It's on by default and can be disabled at Settings → Caching → Performance.
Tier 11 — Edge caching (the page never reaches PHP at all)
Every tier above makes the work cheaper. This one removes the request. When a page is fully static — no login state, no live widget — WebProCMS can hand it to the layer in front of the site (Cloudflare on most installs, LiteSpeed Cache on an OpenLiteSpeed server, fastcgi_cache on nginx) and let that layer answer repeat visitors directly. PHP doesn't boot. The database isn't opened. The response comes off the edge in single-digit milliseconds.
Two things had to be true before that was safe, and both are enforced automatically.
The page has to prove it's static. On every page save, WebProCMS re-examines the page — plus the header/footer partials, any injected snippets, and every shared row it embeds, however it embeds them — for anything visitor-specific: a Livewire component, a CSRF token, an @auth block, a session read. Anything found disqualifies the page, as does any access gate (draft, member-only, role-restricted, redirected). Add a live widget to a static page and it silently returns to normal serving on that save; remove it and the page qualifies again. You never manage the list.
Forms no longer cost a page its cache. A contact or application form used to be the classic disqualifier — it needs a session, a security token, and a live component, so one lead-gen form made its whole page dynamic. Now the cached page carries only lightweight placeholder chrome, and the real form boots inside its own small frame — served fresh, with its own session — as the visitor scrolls toward it. Validation, spam protection, file uploads, save-and-resume, prefilled fields, and payment hand-offs all work exactly as before, and lead attribution still records the page the visitor was actually on; the page around the form never stops being an edge hit. Simple signup bands (newsletter, job alerts) stay plain forms and fetch their security token on the visitor's first interaction instead of baking it into the page. The site chat bubble works the same way: the page bakes an inert bubble, and the live chat starts in its own frame on the first click.
Detail pages qualify too. Blog posts, locations, services, and every other content-type detail page — the bulk of most sites' URLs — resolve their record through a plain serving path rather than a live component, and so does any item that's been promoted to its own custom page. The moment their layout is static, they earn the same sessionless edge treatment as any other page; member-gated records are the exception and deliberately keep their session, so a logged-in member is never mistaken for a guest.
A qualifying page stops carrying visitor state at all. Static pages are served without a session, which means no session cookie and no XSRF cookie — a prerequisite, since every edge refuses to cache a response that sets cookies. It's also a real cost saving on its own: before this, every crawler and every bot hitting the site wrote a session file to disk. Staff aren't locked out of their tools by it — the admin edit bar is fetched separately by the browser for logged-in managers, so it still appears on cached pages.
Edits still publish instantly. A short cache lifetime alone would mean waiting out the clock after every save, so WebProCMS purges the edge directly. Every internal cache invalidation — page saves, content records, unpublishing, feature toggles, settings changes — mirrors its exact URL list (language variants included) to the edge: to Cloudflare via its purge API when a zone token is connected, and to LiteSpeed via purge headers on the very response that performed the save. Content reaches visitors in seconds. Because freshness rides on purge rather than expiry, plain page URLs earn a full-day edge lifetime — on Cloudflare once a token is connected, and on LiteSpeed always. URLs carrying a query string (pagination, campaign parameters) keep the short lifetime instead: a purge can't name every variant, so for them expiry stays the freshness mechanism, and browsers always get the short lifetime since a browser cache can't be purged at all.
More than pages ride the edge. Permanent redirects — the map of old-site URLs every migrated install carries — are pure functions of their route line, so LiteSpeed serves them without booting PHP once the first visitor has hit each one; editing a redirect purges its URL like any content edit. The crawler files (robots.txt, sitemap.xml, llms.txt, site.webmanifest), whose traffic is almost entirely bots, are edge-served the same way on their existing short lifetimes.
On LiteSpeed servers it turns itself on. Managed OpenLiteSpeed hosts (RunCloud-provisioned servers among them) typically ship the cache module already enabled but idle — it stores nothing until the application explicitly marks a response as cacheable, which is exactly the signal WebProCMS sends. So on LiteSpeed there is usually nothing to configure at the server at all: the CMS detects the server on first request and switches edge caching on for you. Turning it back off is respected permanently. Everywhere else it stays an explicit choice at Settings → Caching → Performance, alongside the cache lifetime.
Images barely need the edge now. Everything above is about pages, and a page is the hard case — it can carry a login, a form, a visitor's name. Image variants are answered by the webserver off disk before any of this applies (Tier 5), so the edge is a bandwidth saving for them rather than the thing that keeps PHP out of the path. Where LiteSpeed's cache is available they are held there too, which spares even the disk read.
What you give up, stated plainly. On an edge hit, PHP genuinely does not run — so nothing server-side observes that visit. Analytics is the one place that would have quietly under-counted, so static pages report their views from the browser instead (see Analytics); the practical effect is that those counts become human-only, since bots don't run JavaScript. The remaining trade-off is timing: for the length of the cache lifetime, an edge copy can outlive a change to who's allowed to see the page. That's why gated pages never qualify in the first place, why the default lifetime is short, and why enabling Coming Soon or Maintenance mode clears the edge immediately.
Tier 12 — Staggered script start-up
The tiers above get the page to the visitor. This one keeps the phone responsive once it arrives.
A page's interactive behaviours — menus, dropdowns, accordions, sliders, the overlays — are switched on by one small library as the page finishes loading. It used to do all of that in a single unbroken burst of work. On a fast laptop that is invisible. On the mid-range phone Google scores sites against, it is long enough that the browser cannot paint, cannot respond to a tap, and counts the whole stretch against the page's performance score.
The same work is now done in short steps. The header switches on first, then the page's rows in the order they appear, then the overlays that stay hidden until something opens them. Between each step the browser gets a moment to paint or answer a tap. Nothing becomes interactive later than it did before, and no work was removed — the pauses are the entire point.
It applies to pages served with the slim script bundle (Tier 11's static pages, in practice most of a site). It is on by default and can be turned off at Settings → Caching → Performance, which is worth doing only if a custom script on the site expects every behaviour to be ready the instant the page loads.
Shedding bot traffic before it costs anything
Caching makes a page cheap. The cheapest request, though, is one the site never
handles at all — and on a typical site a large share of traffic is bots probing
for WordPress paths (/wp-login.php, /wp-admin/…, /wp-content/…,
/xmlrpc.php). None of those exist here, so every one of them is pure waste.
Left alone they still cost a full framework boot to produce a 404: autoloader,
service providers, routing, session, database.
WebProCMS refuses them at the outermost layer available, in two forms:
The web server rules (automatic). The shipped public/.htaccess carries
mod_rewrite rules that refuse the probe families measured in real fleet access
logs, each 403d by the webserver with PHP never started: the WordPress paths
above; requests for PHP scripts that don't exist on disk (admin.php, shell
drops — real entry points like index.php and recover.php exist as files and
are exempted by that existence, which is the whole allowlist); dotfile paths
(/.env, /.git/…, with /.well-known left reachable for certificate
issuance); Microsoft autodiscover probes; and the old-CMS /site/assets/
tree migrated sites keep getting crawled for. Direct hits on /index.php —
a bot favourite that would otherwise render the full homepage at a duplicate
URL — are 301'd to /. Apache and LiteSpeed — including OpenLiteSpeed, which
reads .htaccess for rewrite rules — honour all of this. Nothing to
configure; it applies on every install that gets a normal update. Flat-layout
installs already refuse all of these probe families through their
document-root .htaccess's default-deny allowlist.
The PHP guard (opt-in). nginx ignores .htaccess entirely, so on that stack
the rule above does nothing. For those servers, Settings → Caching → WordPress
scanner blocking enables a check at the very top of public/index.php that
sends 403 and exits before the Composer autoloader loads. It still spends a
PHP worker, but skips the expensive part — framework boot, session, and every
query behind it.
The toggle is off by default, because the .htaccess rule already covers
Apache and LiteSpeed and there's no reason to add a check that server has
already made. The card tells you which case you're in: on a server that reads
.htaccess it says so and marks the guard redundant; on nginx it recommends
turning it on. Enabling it on a server that already blocks these paths is
harmless — just a second layer that never fires.
Both forms match only at the start of the path, so an ordinary page whose URL
happens to contain wp- (/blog/our-wp-migration-story) is untouched. Because
the guard's state has to be readable before the framework — and therefore before
the settings database — it is stored as a flag file rather than a setting row.
That file lives under storage/, which CMS updates never overwrite, so the
choice survives every update.
The full cache stack on disk
| Layer | What | Where |
|---|---|---|
| Page-data sidecars | Per-page content overrides, opcache-resident | storage/framework/page-data/pages/{slug}.php |
| Preview sidecars | Per-editor-session pending edits for the iframe | storage/framework/page-data/preview/{token}.php |
| Shortcode cache | All active shortcodes | storage/framework/cache/data/ |
| Snippet cache | All active snippets | storage/framework/cache/data/ |
| Media metadata cache | Width/height/focal/version per image path | storage/framework/cache/data/ |
| Glide image variants | Resized/re-encoded versions of every image, served straight off disk | public/img/v/{path}/ |
| Response cache (optional) | Full-page HTML for guests | storage/framework/cache/data/ |
| Edge cache (optional) | Static pages held by Cloudflare / LiteSpeed / nginx, plus image variants on LiteSpeed | the edge layer itself — not on the app server (LiteSpeed's own store lives per web server, e.g. /home/{user}/lscaches/{app}) |
| Runtime CSS supplement | Utilities used by saved content but absent from the built bundle | public/build/runtime-utilities.css |
| Blade view cache | Compiled blade templates | storage/framework/views/ |
| Route cache (prod) | Compiled route list | bootstrap/cache/routes*.php |
| Config cache (prod) | Merged config tree | bootstrap/cache/config.php |
| CSS/JS bundles | Vite-built assets, hashed for cache-busting | public/build/ |
| Scanner-guard flag | Presence enables the pre-boot bot-probe guard | storage/framework/scanner-guard.on |
Every layer is independently invalidated. Editing a shortcode doesn't touch page sidecars. Editing a page doesn't touch the shortcode cache. Replacing an image doesn't rebuild any blade file. The system never has a "rebuild everything" step because there's nothing to rebuild — each layer regenerates itself on first hit after invalidation.
Why this matters
Pure flat-file generators (Jekyll, Hugo, 11ty) are fast because there's no database — but every content change requires a rebuild step, and any dynamic feature (per-user content, search, forms, e-commerce) requires bolting on a separate runtime. They don't work for SaaS, e-commerce, or any site where the page varies per visitor.
Pure database-driven CMSes (WordPress, Drupal) are flexible because everything is queryable at request time — but every page load costs dozens to hundreds of database queries, and dynamic features pile up. Caching plugins help but introduce their own complexity and stale-content problems.
WebProCMS gives you the flat-file speed profile as the baseline and layers cached dynamic features on top — each one with its own invalidation rules, so the layers don't fight each other. You get the searchability and flexibility of a dynamic CMS with the per-request cost of a static site, even on pages where a full-page cache wouldn't work.
This makes it ideal for:
- SaaS websites where logged-in users see personalized dashboards mixed with marketing pages
- E-commerce with per-user cart, pricing, and inventory state
- Member portals, booking systems, learning platforms — anywhere "show this visitor their data" rules out a full-page cache
- Multi-tenant deployments where different visitors see different brand customizations of the same underlying pages
- Content-heavy marketing sites where editors need to update business data, snippets, and images without coordinating a deploy
Bonus: AI-friendly and Git-friendly
Because pages, layouts, shared rows, and routes are all real files on disk, the entire site structure is:
- Diffable in Git. A page edit produces a readable diff of class changes, content updates, and route additions. Reviews, blame, rollback, and branch-based content workflows all work.
- Legible to AI tooling. Code assistants can read the actual page structure to understand layout, propose changes, and generate matching components. Compare to a CMS where the entire site is hidden inside a database — there's nothing for the AI to read.
- Greppable. Find every page using a row, every shortcode used in the site, every shared include — all with ordinary file search.
- Backupable with
git push. Half of your "backup" is just the repo.
The flat-file foundation isn't just a performance choice — it's what makes the codebase coherent to humans, tools, and AIs alike.