backstage/laravel-static
Backstage Laravel Static helps you add static content and prebuilt HTML pages to Laravel apps, combining fast, cache-friendly delivery with familiar routing, views, and Artisan workflows.
Install with composer require backstagephp/laravel-static, publish the package config, and attach the package middleware (or per-route opt-in — check the README for the exact class/tag names, as they change pre-1.0) to the routes you want cached. Cached files are written per-URI to the configured local disk.
Look first at three things:
static:status prints back to you, so the numbers and the knobs are on one screen.php artisan static:status — new in v0.11.0. Run it before and after warming the cache; it's the fastest way to see what is actually on disk.php artisan route:list — decide which routes are safe to cache (GET, no session/auth-dependent output) before turning the middleware loose.First use case: cache one marketing or blog page, hit it, then run static:status. Now append ?utm_source=test to the URL, request it again, and re-run the command. You'll watch a new file appear as its own cached page. That single exercise explains the majority of "why is my static cache huge" incidents.
Cache-then-measure loop. Treat warming and static:status as a pair. Warm the routes, read the status output, and only then decide whether to enable compression or prune. The command is cheap and read-only; there is no reason to guess.
Query-string discipline at the edge. Because a file is written per-URI including the query string, any page that accepts tracking parameters (utm_*, fbclid, gclid) will slowly multiply. Two patterns work well:
static:clear --only-with-query-strings (with --disk= and, if you want to be selective, --limit=) on a cron. The status output's "top URLs by cached variants" and "most common query parameters" tables tell you exactly which parameter to add to your strip list.Compression is a size lever, not a page-count lever. Enabling gzip/brotli adds compressed siblings on disk (which count toward size) but not toward the page count. That distinction is deliberate in the status output — it means turning compression on will never read as cache growth, so you can enable it without invalidating your monitoring baselines.
Monitoring and alerting. Use static:status --json from a scheduled command or an external check, and alert on size on disk or on the query-string-variant ratio (the WARN line in human output). Pair the alert with the pruning command so a threshold breach has an automated response. The JSON output is stable enough for a disk-usage probe; the human output is for humans.
Multi-disk setups. static:status --disk= inspects a specific disk, while static:clear --only-with-query-strings scopes the prune. If you cache tenant sites or multiple content types on separate disks, run status per disk in your dashboard rather than aggregating — the per-host breakdown in the status output can conflate tenants that share a disk.
Extension point: scheduled maintenance. A sensible default is a nightly static:status --json (log the numbers) plus a weekly or size-triggered static:clear --only-with-query-strings. This keeps the cache from silently accumulating newsletter/bot variants without you needing to remember to check.
static:clear. There is no LRU/TTL eviction built into what this release describes — file age distribution is reported, not enforced. If you need bounded growth, you have to schedule the prune yourself.utm_source or fbclid, prune first, optimize later.--only-with-query-strings is the safe prune. It reclaims the variant space while leaving your canonical, parameter-free pages intact — the pages you'd have to re-warm. Prefer it over a full static:clear whenever the cause is tracking parameters.du-style output collapses pass-through levels. Large-directory totals are cumulative with single-entry intermediate directories folded away, so a directory with one huge subdirectory shows up at the top rather than its child — don't misread a collapsed level as a single directory of that size.static:status (and presumably the --disk= option) refuses remote disks because walking one would be an API call per file. If your cache lives on S3, the command has nothing to say; you'll need a different observability path there.composer.json and re-check the changelog before upgrading — read each release note for renamed flags rather than assuming --only-with-query-strings will keep its spelling.static:status --limit=N then --json is the right order: use the limited human view to find the anomaly, then pipe the JSON into jq for exact numbers before writing the alert rule.How can I help you explore Laravel packages today?