HTML Cache Invalidation
HTML Cache stores cached HTML files, an index of which models were seen while rendering each URL, and optionally a stale URL queue for scheduled refreshes. Clear or refresh both when a package changes content that may already be cached.
Invalidation Modes
Section titled “Invalidation Modes”The default invalidation mode is instant: model and routing changes enqueue durable after-commit jobs that delete affected cached files as soon as a queue worker accepts them. Enable scheduled invalidation with:
CAPELL_HTML_CACHE_INVALIDATION_MODE=scheduledCAPELL_HTML_CACHE_INVALIDATION_SCHEDULE=everyFiveMinutesCAPELL_HTML_CACHE_INVALIDATION_BATCH_SIZE=100CAPELL_HTML_CACHE_PROCESSING_TIMEOUT_MINUTES=15CAPELL_HTML_CACHE_RETRY_BACKOFF_MINUTES=5CAPELL_HTML_CACHE_MAX_ATTEMPTS=5In scheduled mode, model changes insert rows into stale_cached_urls instead of deleting cached files immediately. The scheduler runs capell:html-cache:process-stale on the configured cadence, renders fresh HTML through the Laravel kernel, and atomically replaces the existing cache file only after the refreshed response is safe and cacheable.
Site-domain scheme, host, path, site, and language mutations enqueue a full cache-clear job immediately after commit, even in scheduled mode. Those changes can make the old file path or public URL unsafe to serve while waiting for the next scheduled refresh cycle.
Route/structure model creates and deletes still trigger broad invalidation because they can change public URL resolution. Non-route model creates and translation updates use the dependency index instead: only cached URLs that previously recorded that model are cleared in instant mode or marked stale in scheduled mode. This avoids cold-starting the full cache when a leaf record is created.
Domain non-resolution is treated as an obsolete-domain policy, including preview-excluded domains and temporarily disabled sites. If a stale URL no longer resolves to an enabled site domain, the processor treats that as confirmation that the old public cache entry is obsolete. While caching and cache writes are enabled, it deletes the old cache files and removes matching cached_model_urls rows; otherwise it leaves the artefacts intact and the refresh retryable.
Cache Root Layout
Section titled “Cache Root Layout”An absent port, port 80 and port 443 retain the existing layout byte for byte,
regardless of perceived scheme or whether the port is an integer or string:
http.example.test and https.example.test. Homepage files remain pc__index__pc.html; /form remains
form.html, with form.404.html for a cached 404. This is a compatibility contract:
nginx reads these files directly using a configured scheme/host root, for example
set $capell_page_cache_root /page-cache/https.capell.app, followed by
try_files $capell_page_cache_root/pc__index__pc.html or
$capell_page_cache_root$uri.html before falling back to PHP.
Only other ports add a separate top-level component, for example
~port-8080/http.example.test/form.html or ~port-8443/https.example.test/form.html.
A standard scheme/host root can never match this component. Request ports come from the effective origin, including
forwarded headers only when the proxy is trusted. Domain-relative paths use the
configured site-domain origin, including its port. Eligibility diagnostics select
the inspected origin only for non-standard ports; standard-port diagnostics retain
their historical ambient request root, including cross-scheme inspections. Historical
domain overrides still identify their owning scheme/host; the URL supplies the
serving port.
Stale and static warmers retain a Host port only for non-standard ports. Both 80 and 443 retain the historical bare Host header, even across schemes, and therefore retain the scheme’s effective request port and site-selection behaviour. For non-standard ports, refresh publication derives the current request key even when an old stale row stores a root from before port isolation, and does not overwrite standard-port HTML. Standard ports publish to the stored path unchanged, including historical keys and domain aliases. Port-only domain saves retain the existing site-surrogate invalidation; they do not add a full-clear job. Domain invalidation does not automatically warm pages: the next cacheable request rebuilds a cleared entry. Exact purges stay inside the resolved origin root. URL hashes and stale keys already retain ports; withdrawal counters deliberately fence every port and query variant of a host/path, and the publication generation remains shared by the disk.
Broad model and page-surrogate clears discover sibling port roots on disk as well as indexed URLs, so a queued dependency-registration job cannot leave another port’s copy readable. Site-surrogate clearing also removes matching non-standard domain directories, including mounted site paths, when pages have no model index. It fences in-flight publication even before an origin directory exists. Unindexed standard-port directories retain their existing policy; exact URL clears still preserve neighbouring ports. Native URL deletion strips the complete domain root before checking descendants: a port root has two components, not one.
Publication and generic filesystem operations verify the resolved target or its deepest existing ancestor against the resolved cache root before creating directories, writing or deleting. Pre-existing symlinks cannot redirect those operations outside the disk; tracked URL deletion additionally refuses symlinked descendants within its owning domain.
Stored Keys And Exact Clearing
Section titled “Stored Keys And Exact Clearing”Relative stored keys are unchanged. Allow-listed query parameters and the X-Fragment marker still share one ~<16-hex> hash; requests without either use the canonical filename without a hash. Existing standard-port full-response and combined-fragment files remain readable after upgrade. Non-standard ports use the isolated roots above. Publication does not add a separate ~f fragment suffix.
URL clearing and retirement reconcile current keys with historical row and index paths, removing HTML, 404 files and fragment sidecars within the owning site/domain directory. Stored paths cannot cross the cache root or follow symlinks. When historical ownership cannot be resolved, explicit clears delete the selected tracking rows and purge the edge URL, skip filesystem deletion, and return false; observer and job callers do not throw or guess a filename. Artefacts attributable to a URL through its request key, cached_model_urls, or recorded stale_cached_urls paths are cleared exactly. Recorded neighbours, including other query variants, retain their files byte-for-byte and their index rows. Independently keyed ~f fragment artefacts, where present, are also attributable to their query variant.
Conservative Eviction Of Unattributable Legacy Artefacts
Section titled “Conservative Eviction Of Unattributable Legacy Artefacts”The narrow exception is an unrecorded ~<16-hex> HTML or 404 filename (or its fragment sidecar) on the same page path in the same site/domain. Legacy fragment files and untracked full-response query-variant files share this opaque form: their names cannot distinguish the two or recover their URL ownership. Both are conservatively evicted, because over-eviction only costs regeneration and retaining stale content is unsafe.
For example, clearing /page?page=2 also removes an untracked /page?page=3 full response and its sidecars. A tracked /page?page=3 survives byte-for-byte. The canonical /page entry has no hash and always survives that query-variant clear, even without tracking. Other page paths, sites and domains remain outside the exception.
Request-specific fragment retirement clears the selected attributable fragment, subject to this same legacy exception. URL-wide retirement and CLI clearing remove the URL’s attributable fragment family through the shared deletion Action. URLs without a safe cache key bypass retirement and are not queued for refresh.
Middleware checks deterministic origin policy before rejecting unsafe shared HTML, synthetic responses, non-HTML fragments or incomplete capture data. Retirement uses the original response directives, accepts only 2xx/301/308, preserves the same key/site/domain scope and honours inline edge-purge suppression. Disabled caching prevents automatic retirement. Stored key derivation remains independent of cache enablement so explicit clears can still remove existing pagination variants within their exact scope. Visitor bypasses cannot retire anonymous entries. Failed fragment rendering, publication/write failures preserve previous artefacts because they do not establish a lasting policy change; an incomplete capture with otherwise eligible output also preserves them. A cacheable 404 refresh publishes the new error HTML and removes the previous 200 HTML and its sidecar. Cache hits and coalescing retries have no new origin policy to inspect. Every middleware path records an explicit origin decision; consumers never infer retirement from headers rewritten outside that boundary. Published HTML is accepted even when outer middleware rewrites Cache-Control or Content-Type. Fragment-capture and cookie-stripping helper returns rejoin the miss path; response-header returns occur after retirement checks and cannot turn middleware-added privacy into a retirement reason.
Failed refreshes retry after the configured backoff until max_attempts is reached. Rows that keep failing are marked exhausted instead of being retried forever. Their previous artefacts remain available because transient failures do not justify eviction. The package health diagnostic fails while exhausted rows exist; inspect last_error, repair the cause, then mark the URL stale again or explicitly clear it. Deterministic ineligibility discovered during a refresh — such as a configured bypass path, a package that blocks shared caching, or a redirect response — is marked not_applicable and is not retried. Transient failures remain failed/exhausted and continue to signal an unfinished refresh.
Every successful URL clear or stale refresh also queues an edge purge. A full local clear and global maintenance transitions queue a complete edge purge. Purge failures retry independently, so a temporary CDN API failure does not roll back the origin cache operation.
Cold Requests And Origin Refreshes
Section titled “Cold Requests And Origin Refreshes”Cold requests coalesce behind a bounded cache-lock wait when cache writes are
enabled. A contender rechecks eligible cached HTML after waiting; if none is
available and a cache write is still expected, it receives a translated 503
response with Retry-After and private, no-store. A process-held render lock
prevents overlapping renders when the cache lease expires. These persistent lock
files sit beside the publication lock, outside the page-cache tree; do not unlink
them while workers are running. Shared page-cache nodes must share this lock
directory too. An unavailable render lock is reported and the request renders
normally without coalescing; publication safety checks still apply.
If the owner finishes without writing cacheable HTML, a short per-URL negative
marker lets waiting and subsequent requests render normally. This covers private
or cookie-bearing responses, redirects, server errors and rejected cache writes.
CAPELL_HTML_CACHE_COALESCING_UNCACHEABLE_SECONDS controls the marker lifetime
(default 5 seconds, minimum 1). After expiry, the next miss can try coalescing
again. The marker stores no response bytes and does not change cache eligibility.
PHP-served cache hits schedule a unique origin-refresh job, with at most one
scheduling attempt per URL per cooldown (default 30 seconds). A continuously hot
URL can therefore schedule one job every cooldown even when it is already fresh;
the worker checks whether a stale row needs refreshing. No stale-row query or
full render runs during request termination. Set
CAPELL_HTML_CACHE_ORIGIN_SWR_CONNECTION to a
durable database, redis, sqs or beanstalkd connection, or leave it unset to
use the default queue connection. The worker must have access to the same
page-cache files as the web node. Sync, deferred, background and failover
connections leave regeneration to capell:html-cache:process-stale; they never
run the refresh inline. CAPELL_HTML_CACHE_ORIGIN_SWR_DISPATCH_INTERVAL_SECONDS
controls the scheduling interval.
If a worker dies while holding the job’s unique lock, new jobs for that URL can
remain suppressed until the lock expires. Its lifetime is
invalidation.processing_timeout_minutes (default 15 minutes), measured from
lock acquisition. capell:html-cache:process-stale still covers the durable stale
row independently of that job lock, subject to claim expiry and retry backoff.
Broker failures retain the stale bytes, cooldown and unclaimed durable stale row for a later hit or the stale processor. Hit-telemetry delivery failures are also reported without interrupting stale serving. Claim expiry, retry backoff and publication fencing are applied by the existing refresh action in the queue worker.
Hit-telemetry flush jobs now use beforeCommit() instead of afterCommit().
Their counters are buffered in cache and do not depend on an enclosing database
transaction committing. Immediate dispatch keeps broker failures inside the
telemetry Action’s reporting boundary; a deferred after-commit dispatch could
throw later, outside that boundary, and interrupt an otherwise valid cached
response or its origin-refresh scheduling.
Preventing Publication After Invalidation
Section titled “Preventing Publication After Invalidation”Cache misses capture a publication generation before rendering. Hard URL, directory and full-cache deletions advance that generation under the same POSIX file lock used for publication. A render that started before deletion cannot restore the deleted HTML or its fragment metadata. Rendering and minification run outside the lock; only publication and deletion hold it. Existing cache hits do not acquire the publication lock.
The generation is shared by the entire cache root, so an unrelated hard clear
also rejects an in-flight miss. The completed response remains readable but is
marked private, no-store; the next request can render and cache current data.
Failure to read or acquire the publication guard also refuses the write and
shared response caching. Invalidation failures throw rather than reporting a
successful clear.
The normal middleware and stale-refresh Action capture automatically. Custom
renderers calling PageCache::cache() must call
HtmlCachePublicationGuard::capture($request) before rendering, then pass
that same request to the writer and check its boolean result. A missing or
failed pre-render capture rejects publication; the writer never creates a
replacement token after rendering. WriteRefreshedHtmlCacheFileAction likewise
requires the original request as its third argument.
The default lock file lives under storage/framework/cache/html-cache-publication,
outside the page-cache tree, so clearing cached files cannot replace its inode.
For shared page storage, configure CAPELL_HTML_CACHE_PUBLICATION_LOCK_PATH to
the same persistent, POSIX-lock-capable file on every web and queue node,
outside the page-cache directory. Setting shared_page_cache=true without this
path refuses publication and raises an error on invalidation. A node-local
lock path does not coordinate shared storage.
Read-only Consequence Planning
Section titled “Read-only Consequence Planning”ResolveCachedUrlsForModelAction::run($model) returns distinct tracked URLs plus a page’s current PageUrl URLs, matching instant model invalidation. It also accepts a morph class and model key. For scheduled model invalidation, pass includePageUrls: false: that path marks only already-tracked URLs stale, so including untracked page URLs would overstate the planned work.
ResolveCachedUrlsForSurrogateKeysAction::run($keys) returns the distinct union selected by page and site surrogate keys, including page URL fallbacks. Use this separately when the save emits surrogate-key invalidation; that path still clears immediately in scheduled mode. Its pageIds() and siteCachedUrls() methods let the clearing Action retain its existing page-first execution and subsequent site-row lookup.
Both resolvers only read the current dependency index and page routes. They do not delete cache files, write stale rows, queue refreshes, or enact invalidation. Callers should resolve an explicit preview on demand and apply the save path’s actual invalidation mode; URL selection alone does not imply that regeneration is scheduled.
Deployment Topology And Purge Reach
Section titled “Deployment Topology And Purge Reach”HTML Cache supports three production topologies:
- One web node with a local
page_cachedisk. - Multiple web or queue nodes mounting the same shared POSIX
page_cachedirectory. - Multiple web nodes with a configured
httporcloudflarepurge driver and public traffic delivered through that cache layer.
Multiple web nodes with separate node-local page_cache directories and the default null purge driver are unsupported. A clear job deletes files only from the node that runs it, so another node can continue serving stale HTML.
Declare the topology so Diagnostics can detect this unsafe combination:
CAPELL_HTML_CACHE_WEB_NODE_COUNT=2CAPELL_HTML_CACHE_SHARED_PAGE_CACHE=falseSet CAPELL_HTML_CACHE_SHARED_PAGE_CACHE=true only when every web and queue node mounts the same POSIX directory. Otherwise configure the HTTP or Cloudflare purge driver. The HTML cache multi-node purge safety health check fails when more than one web node is declared while page_cache is local, shared storage is not acknowledged, and purge.driver is null.
The stale_cached_urls table is not a cross-node purge journal. Scheduled invalidation runs capell:html-cache:process-stale with onOneServer(), and each row has one global claim and processed state. It safely regenerates one shared cache entry, but it cannot make every node consume the same deletion. A correct per-node journal would require stable node identities, membership and retirement rules, per-node acknowledgements, and bounded retention; do not treat scheduled mode as a workaround for node-local multi-node storage.
Personalised Route Bypass Examples
Section titled “Personalised Route Bypass Examples”The package defaults cannot know application-specific routes, cookies, or request headers. Configure every shared URL whose response varies by visitor state, for example:
'bypass' => [ 'paths' => ['/cart', '/cart/*', '/account/*', '/checkout/*'], 'cookies' => ['currency_*', 'customer_segment'], 'headers' => ['Accept-Language', 'X-Geo-Country'],],Use only the rules your application needs. A header rule bypasses caching whenever that header is present; it does not create a cache variant.
Cloudflare
Section titled “Cloudflare”Cloudflare does not cache HTML by default. Configure a Cloudflare Cache Rule that makes the intended anonymous public routes cache eligible, while bypassing admin, account, preview, signed, personalised, and cookie-varying routes. Capell emits Cache-Control, Cache-Tag, and Surrogate-Key headers on cacheable responses.
Production cache rule
Section titled “Production cache rule”The capell.app zone uses an active Cache Rule named Capell anonymous public HTML. It is deliberately narrower than “all requests”:
(http.host eq "capell.app" and http.request.method in {"GET" "HEAD"} and not starts_with(http.request.uri.path, "/admin") and not starts_with(http.request.uri.path, "/api") and not starts_with(http.request.uri.path, "/livewire") and not starts_with(http.request.uri.path, "/filament") and not starts_with(http.request.uri.path, "/login") and not starts_with(http.request.uri.path, "/logout") and not starts_with(http.request.uri.path, "/register") and not starts_with(http.request.uri.path, "/account") and not starts_with(http.request.uri.path, "/checkout") and not starts_with(http.request.uri.path, "/billing") and not starts_with(http.request.uri.path, "/up") and not http.cookie contains "capell-session=")Use these rule settings:
- Cache eligibility: Eligible for cache.
- Edge TTL: Use the origin
Cache-Controlheader when present; bypass cache when it is absent. - Browser TTL: Respect origin TTL.
- Cache key: Keep Cloudflare’s default query-string behaviour. Capell rejects unsafe or unknown variants at the origin.
Do not replace the Edge TTL policy with “ignore cache-control”. Capell relies on private, no-store, and no-cache to keep authenticated, signed, preview, personalised, and otherwise unsafe responses out of the shared edge cache. The path and session-cookie conditions are an additional guard, not a replacement for the origin response policy.
The saved production rule and active rule list are shown below.



After deploying or changing the rule, make two anonymous requests to a public page. The first response should normally be MISS; the second should be HIT and include an Age header. An excluded or private route must remain DYNAMIC or BYPASS:
curl -sS -D - -o /dev/null https://capell.app/ \ | rg -i '^(cache-control|cf-cache-status|age):'curl -sS -D - -o /dev/null https://capell.app/ \ | rg -i '^(cache-control|cf-cache-status|age):'curl -sS -D - -o /dev/null https://capell.app/login \ | rg -i '^(cache-control|cf-cache-status|age):'The production check on 18 July 2026 returned MISS then HIT for / and /pricing. /login returned Cache-Control: no-cache, private and CF-Cache-Status: DYNAMIC on both requests.
Use the native Cloudflare purge adapter:
CAPELL_HTML_CACHE_PURGE_DRIVER=cloudflareCAPELL_HTML_CACHE_PURGE_REQUIRED=trueCAPELL_HTML_CACHE_PURGE_TOKEN=your-zone-cache-purge-tokenCAPELL_HTML_CACHE_CLOUDFLARE_ZONE_ID=your-32-character-zone-idThe API token requires Cache Purge permission for the configured zone. URL purges are the precise baseline for page invalidation; cache tags are used for broader site, language, page, and extension invalidation. When an invalidation contains both, the adapter sends both operations and treats either failure as a failed job. Run capell:html-cache:edge-purge:verify --driver=cloudflare during deployment to validate the selected driver and credential presence without transmitting the token or purging content. Do not configure Cloudflare to cache responses marked private, no-store, or no-cache, or responses that set personalised cookies.
The page_cache disk must expose local filesystem paths because refreshes use atomic file replacement. Use a local disk for a single web node or a shared POSIX filesystem mounted by every web and queue node. Object-storage disks are not supported. Site Health reports an error when the disk cannot provide local paths; the deployment-topology check separately reports unsafe multi-node purge configuration.
Main Actions
Section titled “Main Actions”| Action | Use it when |
|---|---|
ClearCachedUrlAction | You know the public URL that should be removed from the cache. |
ClearCachedPageUrlsAction | You have a collection of URLs and want a simple count of cleared entries. |
ClearCachedUrlsForModelAction | You changed one model and want to clear every cached URL that referenced it. |
MarkCachedUrlStaleAction | You know a URL should be refreshed on the next scheduled cycle. |
MarkCachedUrlsForModelStaleAction | You changed one model and want scheduled mode to refresh indexed URLs. |
MarkAllCachedUrlsStaleAction | Broad routing/domain changes should queue all indexed URLs as stale. |
ProcessStaleHtmlCacheAction | Process pending stale URLs and atomically refresh their cached HTML. |
RecordCachedModelUrlsAction | A render pass knows which models contributed to a cached URL. |
GenerateStaticSiteAction | A full static generation run is needed for one Site. |
GenerateStaticSitesAction | Static generation should run for all selected sites. |
The admin cache map reads cached_model_urls; it does not scan public HTML on every request. If a package renders model-backed content but never records dependencies, cache invalidation can only work by URL.
Clear One URL
Section titled “Clear One URL”use Capell\HtmlCache\Actions\ClearCachedUrlAction;
ClearCachedUrlAction::run('https://example.test/about', refresh: true);refresh: true dispatches Capell\Core\Actions\VisitUrlAction after the file and index rows are removed. Use it only when the URL should be warmed immediately.
Clear Every URL For A Model
Section titled “Clear Every URL For A Model”use Capell\HtmlCache\Actions\ClearCachedUrlsForModelAction;
$cleared = ClearCachedUrlsForModelAction::run($article, refresh: false);This looks up rows where cacheable_type matches $article->getMorphClass() and cacheable_id matches the model key. If the model was never recorded with RecordCachedModelUrlsAction, the action returns 0.
Record Dependencies During Rendering
Section titled “Record Dependencies During Rendering”use Capell\HtmlCache\Actions\RecordCachedModelUrlsAction;
RecordCachedModelUrlsAction::run($url, [ $article->getMorphClass() => [$article->getKey()], $author->getMorphClass() => [$author->getKey()],]);RecordCachedModelUrlsAction resolves the site domain and path from the URL, upserts the current dependencies, and removes stale dependencies for the same URL hash.
Configuration
Section titled “Configuration”| Key | Purpose |
|---|---|
capell-html-cache.enabled | Turns HTML cache behaviour on or off. |
capell-html-cache.write_enabled | Allows cache writes. Disable this when investigating output safety. |
capell-html-cache.minify_html | Controls minification before writing cached HTML. |
capell-html-cache.cache_ttl | Backward-compatible source for shared HTTP cache age when no explicit shared_max_age is configured. |
capell-html-cache.filesystem_ttl_seconds | Hard expiry backstop for filesystem entries; 0 disables time-based expiry. |
capell-html-cache.error_pages.max_files_per_host | Maximum cached .404.html files per host cache root; 0 disables cached 404 writes. |
capell-html-cache.error_pages.retain_after_prune | Number of newest cached 404 files retained when the per-host cap is reached. |
capell-html-cache.hit_recording.flush_delay_seconds | Delay used to batch cache-hit counters before one queued database update. |
capell-html-cache.hit_recording.buffer_ttl_seconds | Safety TTL for pending cache-hit counters and their scheduled-flush marker. |
capell-html-cache.http_cache.shared_max_age | s-maxage value for public cached responses; defaults to cache_ttl / 6 when unset. |
capell-html-cache.http_cache.browser_max_age | Browser max-age value for public cached responses. |
capell-html-cache.http_cache.stale_while_revalidate | CDN/browser stale-while-revalidate directive value. |
capell-html-cache.request_coalescing.* | Short per-URL lock and wait values that collapse simultaneous origin cache misses into one render. |
capell-html-cache.cache_skip_authenticated | Keeps authenticated responses out of the public cache. |
capell-html-cache.bypass.paths | Wildcard path rules that bypass public cache reads and writes. |
capell-html-cache.bypass.cookies | Wildcard cookie-name rules that bypass public cache reads and writes. Use for personalization or variant cookies that are not encoded in the URL. |
capell-html-cache.bypass.headers | Wildcard header-name rules that bypass public cache reads and writes. Use for shared-URL locale or segment negotiation headers such as Accept-Language. |
capell-html-cache.access_gate.active_area_cache_seconds | Short TTL for the access-gate active-area lookup used by anonymous cache decisions. Set 0 to disable. |
capell-html-cache.invalidation.mode | instant or scheduled. Default instant. |
capell-html-cache.invalidation.schedule | Scheduler frequency for stale processing. Default everyFiveMinutes. |
capell-html-cache.invalidation.batch_size | Default stale URL batch size. |
capell-html-cache.invalidation.processing_timeout_minutes | Minutes before a processing stale row may be claimed again. |
capell-html-cache.invalidation.retry_backoff_minutes | Minutes before a failed stale row may be retried. |
capell-html-cache.invalidation.max_attempts | Maximum refresh attempts before a stale row becomes exhausted. |
capell-html-cache.retention.processed_stale_days | Days to retain completed stale-refresh rows for diagnostics. |
capell-html-cache.retention.generation_run_days | Days to retain completed or failed static-generation run records. |
capell-html-cache.purge.driver | Edge purge adapter: null, http, or cloudflare. |
capell-html-cache.purge.required | Makes the null driver fail publication invalidation instead of succeeding as a local-only no-op. |
capell-html-cache.purge.token | Bearer token for the configured edge purge API. |
capell-html-cache.purge.cloudflare.zone_id | Cloudflare zone ID used by the native purge adapter. |
capell-html-cache.deployment.web_node_count | Number of web nodes that may serve public HTML; used by the deployment-topology health diagnostic. |
capell-html-cache.deployment.shared_page_cache | Acknowledges that every web and queue node mounts the same POSIX page_cache directory. |
capell-html-cache.model_event_registration_mode | Controls model event registration timing; default is deferred. |
capell-html-cache.static_generation.internal_requests | Renders static generation through the current Laravel kernel; enabled by default for reliable completion reporting. |
capell-html-cache.public_html_authoring_markers | Strings used by diagnostics to detect authoring leakage in public HTML. |
Hit telemetry is operational metadata, not a durable event log. Pending counters and the scheduled-flush marker expire after hit_recording.buffer_ttl_seconds; if queue workers remain unavailable longer than that window, some hit and byte totals can be lost. This does not affect cached HTML correctness. Monitor queue workers and set the TTL longer than the longest outage for which telemetry must be retained.
Console
Section titled “Console”The package command is:
capell:html-cache:process-stale {--limit=}capell:html-cache:diagnose {url?} {--site=} {--render} {--json}capell:html-cache:edge-purge:verify {--driver=}capell:static-site {--site=} {--internal} {--refresh}capell:html-cache:process-stale is scheduled automatically when capell-html-cache.invalidation.mode is scheduled. --limit overrides the configured batch size for one run.
capell:html-cache:clear queues a full refresh; it does not regenerate HTML, so existing cached HTML continues to be served until the stale queue is processed. Use capell:html-cache:clear --process when an explicit CLI maintenance run should queue and process the refresh in one command. HTTP and deployment callers should omit --process and leave processing to the queue or scheduler.
capell:html-cache:diagnose --render renders the URL through the current kernel and reports the actual response status, cache directives, Vary, cookies, and edge tag headers. Without --render, it performs the cheaper request/index eligibility check only.
--internal renders through the current Laravel kernel. --refresh deletes affected cached files before rendering.
Internal static generation and stale-refresh renders are marked as synthetic HTML-cache renders. They still record the current model dependency index for the rendered URL, but they do it synchronously inside the synthetic request instead of deferring or queueing another RegisterCachedModelUrlsJob.
Extension Point
Section titled “Extension Point”Implement Capell\HtmlCache\Contracts\PageCacheNotifiable when a class needs to react after a page cache entry is recorded:
use Capell\HtmlCache\Contracts\PageCacheNotifiable;use Illuminate\Database\Eloquent\Model;
final class SearchIndexCacheNotifier implements PageCacheNotifiable{ public function notifyPageCached(Model $model): void { // Keep side effects small; this runs from cache recording paths. }}Keep notifications cheap. Cache writes happen on public page renders, so slow work belongs on a queue.
Public Output Safety
Section titled “Public Output Safety”HTML Cache must remain safe for anonymous visitors, signed-in users, admins, crawlers, and static exports. Do not put authoring attributes, model IDs, signed editor URLs, field paths, package names, or permission hints into cached HTML. If a package needs admin editing, use Frontend Authoring’s post-load beacon.
Cache paths are also part of the public-output safety boundary. Encoded or double-encoded dot segments, control bytes, backslashes, and oversized path segments are rejected before cache reads or writes. Invalid paths are treated as uncacheable instead of being normalized into a nearby public cache file.