backstage/laravel-static 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

backstage/laravel-static

Composer 安装命令:

composer require backstage/laravel-static

包简介

Serving your Laravel app with speed using static caching

README 文档

README

Total Downloads Tests PHPStan GitHub release (latest by date) Packagist PHP Version Support Latest Version on Packagist

Supercharge your Laravel application with static file caching. Laravel Static converts your dynamic Laravel responses into static HTML files, dramatically improving performance and reducing server load.

Why Laravel Static?

Traditional Laravel applications generate HTML on every request, hitting your database and executing PHP code repeatedly. Laravel Static solves this by:

  • Converting dynamic responses to static HTML files — Serve pre-generated HTML instead of executing PHP on every request
  • Reducing server load — Let your web server (Nginx, Apache) serve static files directly
  • Improving response times — Static files are served in milliseconds, not hundreds of milliseconds
  • Supporting multiple caching strategies — Choose between route-based caching or automatic web crawling
  • Handling complex scenarios — Multi-domain support, query string handling, and HTML minification

Requirements

  • PHP 8.1 or higher
  • Laravel 11.0 or higher

Installation

Install the package via Composer:

composer require backstage/laravel-static

Publish the configuration file:

php artisan vendor:publish --tag="laravel-static-config"

Optionally, publish the migrations if you need database-backed features:

php artisan vendor:publish --tag="laravel-static-migrations"
php artisan migrate

Quick Start

1. Enable Static Caching

Add the STATIC_ENABLED=true environment variable to your .env file:

STATIC_ENABLED=true

2. Add Middleware to Routes

Apply the StaticResponse middleware to routes you want to cache:

use Backstage\LaravelStatic\Middleware\StaticResponse;

Route::get('/', function () {
    return view('welcome');
})->middleware(StaticResponse::class);

// Or apply to route groups
Route::middleware([StaticResponse::class])->group(function () {
    Route::get('/about', [PageController::class, 'about']);
    Route::get('/contact', [PageController::class, 'contact']);
    Route::get('/blog', [BlogController::class, 'index']);
});

3. Build the Static Cache

Generate your static files:

php artisan static:build

That's it! Your routes are now served as static HTML files.

Configuration

The configuration file is located at config/static.php. Here's a breakdown of all available options:

Caching Driver

'driver' => 'crawler', // Options: 'crawler' or 'routes'
Driver Description
crawler Uses Spatie Crawler to automatically discover and cache all internal URLs starting from your homepage. Best for sites with many interconnected pages.
routes Only caches routes that have the StaticResponse middleware explicitly applied. Best for selective caching.

Enable/Disable

'enabled' => env('STATIC_ENABLED', true),

Toggle static caching on or off. Useful for disabling in development while keeping it enabled in production.

Build Settings

'build' => [
    'clear_before_start' => true,    // Clear existing cache before rebuilding
    'concurrency' => 5,               // Number of concurrent HTTP requests
    'accept_no_follow' => true,       // Follow nofollow links when crawling
    'default_scheme' => 'https',      // URL scheme for crawler requests
    'force_root_url' => env('STATIC_FORCE_ROOT_URL', false), // Force generated links to app.url during builds
    'crawl_observer' => \Backstage\LaravelStatic\Crawler\StaticCrawlObserver::class,
    'crawl_profile' => \Spatie\Crawler\CrawlProfiles\CrawlInternalUrls::class,
    'bypass_header' => [
        'name' => 'X-Laravel-Static',
        'value' => 'off',
    ],
],

Host Whitelist

'whitelist' => [
    'hosts' => null,  // null = cache every host; array = only these hosts
],

Restrict which hostnames static caches may be created for. When null (the default), caches are created for every host that hits the middleware. Provide an array of hostnames to only cache those hosts — useful when your app is reachable through multiple domains (e.g. a staging or preview domain) but you only want to cache the canonical ones.

Hostnames are matched against the request host (case-insensitive) and support * wildcards — e.g. *.example.com matches any subdomain (but not the apex example.com, which must be listed separately). To restrict caching to your app's own hostname and its www variant:

'whitelist' => [
    'hosts' => [
        $host = parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST),
        'www.' . $host,
    ],
],

File Storage

'files' => [
    'disk' => env('STATIC_DISK', 'public'),  // Laravel filesystem disk
    'include_domain' => true,                 // Create separate caches per domain
    'include_query_string' => true,           // Include query strings in cache keys
    'filepath_max_length' => 4096,            // Maximum file path length
    'filename_max_length' => 255,             // Maximum filename length
],

Additional Options

'options' => [
    'on_termination' => false,  // Save cache after response sent (async)
    'minify_html' => false,     // Minify HTML before caching
],

Commands

Build Static Cache

Generate static files for all configured routes:

php artisan static:build

When using the routes driver, only routes with the StaticResponse middleware are cached. When using the crawler driver, the crawler starts from your homepage and discovers all internal links.

Rebuild a single page instead of crawling the whole site — the targeted counterpart to static:clear --uri:

php artisan static:build https://example.com/blog/my-post

The page is re-rendered through its own route (no clear, no crawl), so only that one cached file is refreshed.

Clear Static Cache

Clear all cached static files:

php artisan static:clear

Clear specific URIs:

php artisan static:clear --uri=/about --uri=/contact

Clear by route names:

php artisan static:clear --routes=home --routes=about --routes=blog.index

Clear by domain (useful for multi-tenant applications):

php artisan static:clear --domain=example.com
php artisan static:clear --domain=subdomain.example.com

Advanced Usage

Multi-Domain Support

Laravel Static supports multi-domain setups out of the box. When include_domain is enabled (default), each domain gets its own cache directory:

storage/app/public/
├── example.com/
│   ├── GET/
│   │   ├── index.html
│   │   └── about.html
├── subdomain.example.com/
│   ├── GET/
│   │   └── index.html

Query String Handling

When include_query_string is enabled, different query strings create separate cache files:

/products?page=1  → products/page=1.html
/products?page=2  → products/page=2.html
/search?q=laravel → search/q=laravel.html

HTML Minification

Enable HTML minification to reduce file sizes:

// config/static.php
'options' => [
    'minify_html' => true,
],

This removes unnecessary whitespace, comments, and optimizes the HTML output using the voku/html-min library.

Bypass Header

During development or testing, you may want to bypass the static cache. The package includes a bypass header mechanism:

curl -H "X-Laravel-Static: off" https://example.com/

This header tells the middleware to skip the static cache and generate a fresh response.

Programmatic Cache Clearing

Use the StaticCache facade to clear cache programmatically:

use Backstage\LaravelStatic\Facades\StaticCache;

// Clear all cache
StaticCache::clear();

// Clear specific paths
StaticCache::clear(['/about', '/contact']);

Programmatic Page Building

Refresh one or more pages without rebuilding the whole site. Each URL is re-rendered through its own route, so only those cached files are written — the targeted counterpart to clear():

use Backstage\Static\Laravel\Facades\StaticCache;

// Rebuild a single page
StaticCache::build('https://example.com/blog/my-post');

// Rebuild several at once
StaticCache::build([
    'https://example.com/blog/my-post',
    'https://example.com/blog',
]);

To do this off the request cycle, dispatch the queued BuildStaticPage job:

use Backstage\Static\Laravel\Jobs\BuildStaticPage;

BuildStaticPage::dispatch($post->url());

The job is a no-op when static caching is disabled, and a failed render is reported rather than thrown — the page keeps its previous cached copy until the next build.

Custom Crawl Observer

Create a custom crawl observer to customize the crawling behavior:

namespace App\Crawlers;

use Backstage\LaravelStatic\Crawler\StaticCrawlObserver;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\ResponseInterface;

class CustomCrawlObserver extends StaticCrawlObserver
{
    public function crawled(UriInterface $url, ResponseInterface $response, ?UriInterface $foundOnUrl = null): void
    {
        // Add custom logic before caching
        logger()->info("Caching: {$url}");

        parent::crawled($url, $response, $foundOnUrl);
    }
}

Update your configuration:

'build' => [
    'crawl_observer' => \App\Crawlers\CustomCrawlObserver::class,
],

Custom Crawl Profile

Control which URLs get crawled by creating a custom crawl profile:

namespace App\Crawlers;

use Psr\Http\Message\UriInterface;
use Spatie\Crawler\CrawlProfiles\CrawlProfile;

class CustomCrawlProfile extends CrawlProfile
{
    public function shouldCrawl(UriInterface $url): bool
    {
        $path = $url->getPath();

        // Skip admin routes
        if (str_starts_with($path, '/admin')) {
            return false;
        }

        // Skip API routes
        if (str_starts_with($path, '/api')) {
            return false;
        }

        return true;
    }
}

Excluding Routes from Caching

Routes with parameters cannot be automatically cached (they require specific values). You can also explicitly exclude routes by not applying the middleware:

// These routes will be cached
Route::middleware([StaticResponse::class])->group(function () {
    Route::get('/', [HomeController::class, 'index']);
    Route::get('/about', [PageController::class, 'about']);
});

// These routes will NOT be cached (no middleware)
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::get('/user/{id}', [UserController::class, 'show']); // Has parameters

Preventing index.php in Cached URLs

If your web server isn't rewriting pretty URLs (missing try_files / mod_rewrite), Laravel derives the request root from index.php and can leak it into generated links and cache paths — e.g. /index.php/about instead of /about.

The package guards against this in two ways:

  • Path normalization (always on): a leading index.php segment is stripped from the cached file path, so a page requested as /index.php/about is still cached as about.html. This applies to both drivers.

  • Force root URL (opt-in): enable force_root_url so that during a build the root URL used by url(), route(), and asset() is forced to config('app.url'), keeping index.php out of the links your pages contain:

    // config/static.php
    'build' => [
        'force_root_url' => true,
    ],
    STATIC_FORCE_ROOT_URL=true

    This affects the routes driver (which renders pages in the build process). The crawler driver renders each page in a separate HTTP request, so for it the path normalization above is what keeps cache paths clean — the real fix there is correcting your server's URL rewriting.

Async Cache Generation

Enable on_termination to generate cache files after the response is sent to the user:

'options' => [
    'on_termination' => true,
],

This improves perceived performance as users don't wait for the cache file to be written.

Web Server Configuration

For optimal performance, configure your web server to serve static files directly without hitting PHP.

Nginx

server {
    listen 80;
    server_name example.com;
    root /var/www/html/public;

    # Try static cache first, then Laravel
    location / {
        # Check for static cache file
        set $cache_path /storage/example.com/GET$uri;

        # Handle index files
        if (-f $document_root$cache_path/index.html) {
            rewrite ^ $cache_path/index.html last;
        }

        # Handle direct files
        if (-f $document_root$cache_path.html) {
            rewrite ^ $cache_path.html last;
        }

        # Fall back to Laravel
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Apache

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Check for static cache
    RewriteCond %{DOCUMENT_ROOT}/storage/%{HTTP_HOST}/GET%{REQUEST_URI}.html -f
    RewriteRule ^(.*)$ /storage/%{HTTP_HOST}/GET/$1.html [L]

    RewriteCond %{DOCUMENT_ROOT}/storage/%{HTTP_HOST}/GET%{REQUEST_URI}/index.html -f
    RewriteRule ^(.*)$ /storage/%{HTTP_HOST}/GET/$1/index.html [L]

    # Laravel fallback
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php [L]
</IfModule>

Cache Invalidation Strategies

Event-Based Invalidation

Clear cache when content changes using model events:

use Backstage\LaravelStatic\Facades\StaticCache;

class Post extends Model
{
    protected static function booted()
    {
        static::saved(function (Post $post) {
            StaticCache::clear([
                "/blog/{$post->slug}",
                '/blog',
            ]);
        });

        static::deleted(function (Post $post) {
            StaticCache::clear([
                "/blog/{$post->slug}",
                '/blog',
            ]);
        });
    }
}

Prefer re-rendering the changed page over clearing it when you want the cache to stay warm. Dispatch the BuildStaticPage job so the edited page is rebuilt immediately instead of being regenerated lazily on the next visit:

use Backstage\Static\Laravel\Jobs\BuildStaticPage;

static::updated(function (Post $post) {
    BuildStaticPage::dispatch($post->url());
});

Scheduled Rebuilds

Add a scheduled task to rebuild your cache periodically:

// app/Console/Kernel.php or bootstrap/app.php (Laravel 11+)
Schedule::command('static:build')->daily();

Deploy Hook

Clear and rebuild cache during deployments:

#!/bin/bash
# deploy.sh

php artisan static:clear
php artisan static:build

Comparison: Routes vs Crawler Driver

Feature Routes Driver Crawler Driver
Setup complexity Manual (add middleware to each route) Automatic (discovers all pages)
Control Fine-grained Less control
Speed Faster (only caches specified routes) Slower (crawls entire site)
Discovery Manual Automatic
Best for Selective caching, large apps Content sites, blogs

How It Works

  1. Request Interception: The StaticResponse middleware intercepts outgoing responses
  2. Eligibility Check: Only GET/HEAD requests with 200 OK status are cached
  3. File Generation: HTML content is saved to the configured storage disk
  4. Optional Minification: If enabled, HTML is minified before saving
  5. Directory Structure: Files are organized by domain, HTTP method, and URI path

The PreventStaticResponseMiddleware (automatically registered) handles bypass headers and ensures proper behavior during cache building.

Troubleshooting

Cache Not Being Generated

  1. Ensure STATIC_ENABLED=true is set in your .env
  2. Verify the StaticResponse middleware is applied to your routes
  3. Check that the storage disk is writable
  4. Routes with parameters cannot be cached automatically
  5. Only 200 OK responses are cached

Cache Not Being Served

  1. Verify static files exist in your storage directory
  2. Check web server configuration
  3. Ensure the bypass header is not being sent accidentally

Crawler Not Finding Pages

  1. Check if pages are linked from the homepage
  2. Verify accept_no_follow setting if using rel="nofollow" links
  3. Review your crawl profile configuration
  4. Note: JavaScript-rendered content is not supported

File Path Too Long

If you encounter file path length errors:

  1. Check the filepath_max_length and filename_max_length settings
  2. Consider using shorter URLs or disabling query string caching
  3. The package will skip files that exceed the configured limits

Testing

Run the test suite:

composer test

Run tests with coverage:

composer test-coverage

Run static analysis:

composer analyse

Format code:

composer format

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

Built with Spatie Crawler and voku/HtmlMin.

License

The MIT License (MIT). Please see License File for more information.

backstage/laravel-static 适用场景与选型建议

backstage/laravel-static 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 282 次下载、GitHub Stars 达 5, 最近一次更新时间为 2025 年 02 月 26 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「laravel」 「vormkracht10」 「laravel-static」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 backstage/laravel-static 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 backstage/laravel-static 我们能提供哪些服务?
定制开发 / 二次开发

基于 backstage/laravel-static 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 282
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 5
  • 点击次数: 25
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 5
  • Watchers: 2
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-02-26