spatie/laravel-export
Turn a Laravel app into a static site bundle. Laravel Export crawls every URL, renders HTML pages, copies your public assets, and saves a ready-to-host export with one artisan command—ideal for blogs and admin-managed sites published to static hosting.
Architecture fit
spatie/laravel-export is not a static-site generator in the Jekyll/Astro sense — it is a renderer of an already-running Laravel app. The mechanism is a crawl: the package boots an HTTP client against your application, follows internal links, and writes each response body to a disk as an HTML file, then mirrors public/ (or any include_files mapping) alongside it. That distinction drives the entire evaluation. It fits cleanly when the app is fundamentally a read-mostly content site (blog, marketing pages, docs, catalogue) where every page is addressable by a stable GET URL and renders server-side. It fits poorly when the value of the app is interactive, session-scoped, or client-rendered.
The architecture is thin and layered on well-known Spatie primitives: spatie/crawler for traversal, Laravel's Filesystem abstraction for output, and symfony/process for hooks. There is no proprietary runtime, no templating dialect, no plugin API. For a TPM this is the single most important finding: the package is a convenience wrapper, not a platform. Lock-in is near zero and an exit to a bespoke Artisan command is measured in days, not quarters.
Two design details materially improve fit. First, Exporter is injectable and exposes the content-affecting options (crawl(), paths(), useStreaming()) in code, so route enumeration can be driven from the database rather than a hand-maintained config array — this is what makes it viable for content sites with hundreds of records. Second, the X-Laravel-Export header (added 1.0.0, corrected to apply on the export path in 1.1.1) gives the application a reliable signal that a request is an export render, enabling you to suppress analytics, disable CSRF expectations, or swap to a stripped layout without branching on user agent.
Integration feasibility
Low-to-moderate effort. Installation is one composer require plus an optional config publish; the default config is functional out of the box. The realistic work is not the package, it is the application remediation required to make every page exportable: removing auth gates from otherwise-public content, ensuring routes are GET and idempotent, eliminating writes-on-read (view counters, "last seen" timestamps, session flash) that make output non-deterministic, and deciding how paginated or faceted URLs are enumerated. Budget the integration at roughly 1–2 weeks for a small site with a clean route table, and 3–6 weeks for a mature app with authenticated areas, dynamic query parameters, or client-side rendering anywhere in the critical path.
Technical risk
paths, and there is no coverage assertion telling you the export is complete. A missing page is a 404 in production, discovered by a user.$foundOnUrl surfaced since 0.3.8). Good for correctness, but it means a single flaky route fails the whole build — desirable in CI, hostile if you ever ran it against production.Key questions
/, or do we need an explicit paths manifest sourced from the database?/rss.xml, sitemap.xml, robots.txt), or binary files preserved — and are those covered by paths and include_files?index.html resolution) match what the package writes?Stack fit
Requires a Laravel app on a supported major (10 through 13 are all covered by the 1.1.x–1.3.x line) with PHP 8.1+. It is a require-dev dependency in spirit — it should never ship to production runtime — and its natural home is a CI job, not a developer laptop and certainly not the web server. Output targets any Laravel filesystem disk, so local for artifact upload or an S3 driver for direct-to-bucket publishing are both first-class; the README explicitly supports the S3 path.
Migration path
The package is additive, so there is no migration in the schema sense. The meaningful decision is what role the Laravel app retains. Two viable models:
Compatibility
Verify three things early, in a spike, before committing: (1) the exact Laravel major vs the package line — 1.3.0 for Laravel 13, 1.1.2 for Laravel 12; (2) spatie/crawler ^9.1, which is pulled by 1.4.0 and later and is where streaming originates, so pinning to an older export release removes the memory relief; (3) the target host's handling of extensionless URLs and trailing slashes, which is a hosting concern rather than a package concern but determines whether the bundle works at all.
Sequencing
paths manifest strategy and implement it via the injectable Exporter in a service provider so it stays in sync with the database.X-Laravel-Export header to strip analytics, disable CSRF-dependent forms, and force canonical URLs, so export renders don't diverge from production renders in ways that only surface in the bundle.before hook for asset compilation (npm run build / Yarn) and the after hook for deploy. Keep hook commands in config-as-code, not committed config files with machine-specific absolute paths. In CI, prefer --skip-deploy on PR builds and the full chain only on merge to the release branch.Maintenance
Ongoing cost is low and mostly external: dependency bumps on Laravel majors, and re-validation whenever the target host changes path semantics. Internally, the real maintenance burden is the paths manifest and render-mode conditioning — both live in your codebase and both rot if contributors add routes without knowing the export exists. Encode that as a CI check (export succeeds + link check passes) rather than a convention.
Support
MIT licensed with active, recent maintenance and a visible changelog. Support depth is realistically "read the source" — the package is small enough to audit in an afternoon, which is the correct mitigation for the thin maintainer bandwidth typical of a Spatie side project. Treat Postcardware as a courtesy, not a support contract. Escalation path is the GitHub tracker plus, if it ever matters, your own fork; the surface area is small enough that forking remains feasible indefinitely.
Scaling
Scales with page count and crawl time, not with traffic — and traffic scaling is the entire point, since the artifact is served by a CDN and the app tier is no longer in the request path. Enable use_streaming before the site grows; it is available from 1.4.0 and is the difference between a build that fits in a CI runner and one that OOMs. For large sites, crawl concurrency matters: you are hammering your own application, so run the export against a dedicated environment or replica, never the production pool. Note also that the crawl is inherently sequential in output correctness terms — no incremental builds, no cache, full re-render every time. A 5,000-page site will mean a 5,000-request build; measure that early because it eventually forces a sharded or scheduled export strategy.
Failure modes
/usr/local/bin/yarn). These break across CI images and developer machines; the --skip-before/--skip-after/--skip-all flags exist precisely because hooks are environment-sensitive and should be used deliberately in CI.Ramp-up
A mid-level Laravel engineer can be productive in under a day; the API surface is a config array, one Artisan command, and one injectable class. The ramp-up cost that actually matters is organizational: content authors need to understand that publishing is a build, with latency and a failure surface, rather than a save. Budget a short runbook covering how to trigger a publish, how to read a failed crawl, and what to do when a page is missing from the bundle.
How can I help you explore Laravel packages today?