Skip to content

URL-Keyed Stale Resilience ​

Public CMS pages use a file-primary presentation cache keyed by locale + normalized URL path when MODULAROUS_PRESENTATION_CACHE_STORE=url (default). On a cache HIT, HTML is served from disk without touching Redis or the database.

Admin cache types (record, index, formItem, formattedItem) remain on Redis.

Storage layout ​

storage/framework/cache/modularous-stale-by-url/
  {locale}/
    {sha256(cache_lookup_key)}.html
    {sha256(cache_lookup_key)}.meta

cache_lookup_key is the normalized path, or path?sorted_query when query-aware caching is enabled for that route.

Each .meta file stores publication snapshot fields used at serve time:

  • published, publish_start_date, publish_end_date
  • visibility_profile: singular (day bounds, IsSingular) or standard (datetime, HasScopes)
  • module, route, urlable_type, urlable_id
  • normalized_path, normalized_query, cache_lookup_key
  • expires_at (fresh TTL) and stale_expires_at (SWR window)

Query-parameter cache variants ​

Dynamic public pages (blog search, paginated listings) can vary by allowlisted query params.

Per module route:

php
'BlogLanding' => [
    'presentation_cache_key' => 'path_and_query_allowlist',
    'presentation_cache_query' => ['page', 'searchblogtext'],
    'types' => ['presentationItem' => true],
],

Strategies:

KeyBehavior
path_only (default)Cache key is path only; query params are ignored
path_and_query_allowlistOnly allowlisted params participate in the key; unknown params bypass cache
path_and_queryAll query params are sorted into the key

For serve_first middleware (before route match), map paths in presentationItem.url.path_query:

php
'path_query' => [
    '/blog' => ['page', 'searchblogtext'],
    '/blog/search' => ['page', 'searchblogtext'],
],

Warmup writes the default view (no query params). Other combinations are cached on first request after a miss.

POST requests are never served from URL cache; blog search POST redirects to a GET results URL.

Request flow ​

mermaid
sequenceDiagram
    participant Browser
    participant MW as ServeUrlKeyedStaleMiddleware
    participant Disk as UrlKeyedStaleCache
    participant Gate as StalePublicationGate
    participant CmsCtrl as CmsController

    Browser->>MW: GET /en/pages/blog
    MW->>Disk: get(locale, path)
    Disk->>Gate: meta visible?
    alt URL file HIT + visible
        MW-->>Browser: 200 HTML X-Modularous-Cache URL_HIT
    else URL miss / not visible
        MW->>CmsCtrl: continue pipeline
        CmsCtrl->>CmsCtrl: resolvePublicItem + render
        CmsCtrl->>Disk: put URL file + meta
        CmsCtrl-->>Browser: 200 MISS
    end

ServeUrlKeyedStaleMiddleware runs before route matching (global HTTP middleware when serve_first is enabled) and again is unnecessary on the catch-all stack. When serve_first is on, cached HTML is served even on hosts that are not bound to {@see CmsFrontRouteRegistrar::resolvePublicFrontRouteDomain()} (e.g. admin hostname during local dev), as long as the path is not excluded (admin /system, /api, etc.).

Configuration ​

Full env table and layer resolution: Configuration — Presentation item cache.

php
'presentationItem' => [
    'store' => env('MODULAROUS_PRESENTATION_CACHE_STORE', 'url'),
    'serve_first' => env('MODULAROUS_PRESENTATION_CACHE_SERVE_FIRST', true),
    'swr' => env('MODULAROUS_PRESENTATION_CACHE_SWR', false),
    'stale_ttl' => (int) env('MODULAROUS_PRESENTATION_CACHE_STALE_TTL', 604800),
    'warm_dispatch_cooldown' => (int) env('MODULAROUS_RESOURCE_CACHE_SWR_WARM_COOLDOWN', 600),
    'url' => [
        'driver' => env('MODULAROUS_PRESENTATION_CACHE_URL_DRIVER', 'file'),
        'base_path' => env(
            'MODULAROUS_CACHE_URL_STALE_PATH',
            storage_path('framework/cache/modularous-stale-by-url'),
        ),
    ],
],
Env varConfig keyDefaultMeaning
MODULAROUS_PRESENTATION_CACHE_STOREpresentationItem.storeurlMust be url for this feature
MODULAROUS_PRESENTATION_CACHE_SERVE_FIRSTpresentationItem.serve_firsttrueMiddleware serves disk HTML before controller
MODULAROUS_PRESENTATION_CACHE_SWRpresentationItem.swrfalseController serves stale + warm; middleware ignores this flag
MODULAROUS_PRESENTATION_CACHE_STALE_TTLpresentationItem.stale_ttl604800 (7 days)stale_expires_at in .meta (absolute from write time)
MODULAROUS_CACHE_URL_STALE_PATHpresentationItem.url.base_pathstorage/framework/cache/modularous-stale-by-urlBase directory
MODULAROUS_PRESENTATION_CACHE_URL_DRIVERpresentationItem.url.driverfilefile, shared_file (EFS/NFS); future: redis, s3
MODULAROUS_RESOURCE_CACHE_TTL_PRESENTATION_ITEMttl.presentationItem900Fresh TTL → expires_at in .meta
MODULAROUS_RESOURCE_CACHE_SWR_WARM_COOLDOWNpresentationItem.warm_dispatch_cooldown600Dedup window for WarmPresentationItemJob when SWR is on

Legacy aliases (when new vars unset): MODULAROUS_CACHE_URL_STALE_ENABLED → store=url; MODULAROUS_CACHE_URL_STALE_SERVE_FIRST → serve_first; MODULAROUS_CACHE_URL_STALE_TTL → stale_ttl; MODULAROUS_RESOURCE_CACHE_SWR_STALE_TTL → stale_ttl.

Enable presentationItem per pilot route — URL resilience only applies where that type is configured in config/modularous.php.

Redis presentationItem ​

Public presentationItem no longer reads or writes Redis. Fresh HTML lives in the URL file (expires_at in meta). Id-based StaleFileCache is the store=model path (legacy SWR), not a URL-store fallback — set MODULAROUS_PRESENTATION_CACHE_STORE=model explicitly to use it.

Invalidation ​

On model purge/unpublish/path change, CacheInvalidation forgets URL stale files for all UrlRoute rows tied to the record (locale + normalized_path).

Multi-node scaling ​

Production currently assumes a single app node where local disk is sufficient. The URL presentation cache is accessed through {@see UrlPresentationCacheStoreInterface} — middleware, invalidation, and warmup call the interface, not the concrete filesystem class directly.

Extension point ​

PieceLocation
Contractsrc/Contracts/Cache/UrlPresentationCacheStoreInterface.php
Default driversrc/Services/Cache/FileUrlPresentationCacheDriver.php → wraps UrlKeyedStaleCache
Driver resolutionModularousCacheService::resolveUrlPresentationCacheStore()
Container bindingUrlPresentationCacheStoreInterface::class (singleton via modularous.cache)
Configmodularous.cache.presentationItem.url.driver
php
'presentationItem' => [
    'url' => [
        'driver' => env('MODULAROUS_PRESENTATION_CACHE_URL_DRIVER', 'file'),
        'base_path' => storage_path('framework/cache/modularous-stale-by-url'),
    ],
],
DriverStatusNotes
file (default)ImplementedLocal filesystem at base_path
shared_fileAlias of fileMount EFS/NFS at base_path across nodes — no code change
redisFutureOptional presentationItem replay / coherence layer
s3FutureObject storage sync; needs purge pipeline

Scaling paths ​

OptionNotes
Shared volume (EFS/NFS)Simplest multi-node path: set MODULAROUS_PRESENTATION_CACHE_URL_DRIVER=shared_file (or keep file) and mount modularous-stale-by-url on all app nodes
Object storage + CDNImplement UrlPresentationCacheStoreInterface for S3; serve via CDN; wire purge through webhook or invalidation jobs
Redis presentationItem replayOptional coherence layer when shared disk is not viable; implement driver that replays warm jobs or mirrors HTML blobs

To add a new driver:

  1. Implement UrlPresentationCacheStoreInterface (get/put/forget + forgetByRelation/forgetByModuleRoute/forgetPathVariants + key helpers).
  2. Register in ModularousCacheService::resolveUrlPresentationCacheStore() for your driver config value.
  3. Keep invalidation semantics: purge must clear all query variants for a locale + path.

No multi-server driver beyond file/shared_file is included in the initial rollout.