Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Export Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and run the default export to see what you get before configuring anything:

composer require spatie/laravel-export
php artisan export

Out of the box this crawls your app, renders every reachable URL to HTML through the HTTP kernel, copies public/ into the bundle, and writes it to an export disk (default: dist in the app root per the README's custom-disk example — confirm the path your install produces before wiring CI). The first thing to check is config/filesystem.php; if there's no export disk defined, add one:

'export' => [
    'driver' => 'local',
    'root' => base_path('dist'),
],

Then publish the config only if you need to change behaviour:

php artisan vendor:publish --provider=Spatie\\Export\\ExportServiceProvider

Your first realistic use case: a marketing site or blog where content is managed in Filament/Nova/Wink, and the public front end is a set of show routes you want on Netlify or GitHub Pages. Start by exporting a single page to validate rendering and asset copying, then widen the scope.

Read in this order when something misbehaves: config/export.php (crawl vs. paths, include/exclude rules), the Exporter class (all content options are also available programmatically), then the export command signature (hook-skipping flags).

Implementation Patterns

Crawl mode vs. explicit paths. Crawling is convenient but slow and fragile for large sites — it discovers URLs by following internal links from your entry point. For anything with a sitemap-shaped structure, turn crawling off and enumerate:

// config/export.php
'crawl' => false,
'paths' => ['/', '/about', '/contact', '/rss.xml'],

Or do it in code, which is usually better because you can pull from the database:

use Spatie\Export\Exporter;

public function boot(Exporter $exporter)
{
    $exporter->crawl(false);
    $exporter->paths(['/', '/about']);
    $exporter->paths(Post::where('published', true)->pluck('slug')->map(fn ($s) => "/posts/{$s}"));
}

paths() appends rather than replaces, so you can call it repeatedly from multiple service providers or modules and compose the export surface. This is the extension point most day-to-day work lands on.

Streaming for big crawls. If you must crawl thousands of pages, enable use_streaming to keep memory flat. It has no effect when crawl is false, and it constrains what you can do mid-export (see Gotchas).

Asset pipeline via hooks. Wire your front-end build into the export so a single command produces a deployable bundle:

'before' => [
    'assets' => 'npm run build',
],
'after' => [
    'deploy' => 'netlify deploy --prod --dir=dist',
],

Hook keys become CLI flags. A hook named deploy gives you php artisan export --skip-deploy; --skip-before, --skip-after, and --skip-all cover the groups. Use this in CI: local runs skip deployment, CI runs it fully, and a broken deploy is isolated from the export itself.

Deploying to object storage. Because the destination is a Laravel disk, you can point the export disk at S3 and skip a CI artifact step entirely. Keep in mind the after hook runs after files are written, so a netlify/aws s3 sync hook sees the finished bundle.

Detecting export requests in your app. Exports carry an X-Laravel-Export request header. Use it to branch behaviour — disable analytics snippets, cookie banners, CSRF-heavy widgets, or query strings that shouldn't bake into static HTML:

if (request()->hasHeader('X-Laravel-Export')) {
    // suppress third-party scripts in the layout
}

Redirects. Since 1.2.0 redirect responses are exported rather than treated as failures — useful for legacy URL aliases in a static host that reads meta-refresh or _redirects-style output. Verify the emitted files match what your host expects before relying on it.

Include/exclude for static assets. Defaults copy all of public/ while excluding PHP and mix-manifest.json. Add build outputs that live outside public (e.g. a dist from Vite in a custom location) via include_files, mapping source path to destination path:

'include_files' => [
    'public' => '',
    'storage/app/generated' => 'generated',
],

Gotchas and Tips

Crawling needs a working app, not just a built one. The exporter issues real HTTP requests through the framework. Anything gated behind auth, a middleware that hits an external service, or a route that requires a session will either be skipped, 302'd, or throw. Since 0.3.3 broken pages throw exceptions during crawl and errors include the foundOnUrl, so the failure message tells you which page linked to the bad URL — read it before reaching for crawl => false.

Crawl scope is a footgun. Paginated archives produce near-infinite link chains; faceted filters (?page=2&sort=...) explode the page count. Exclude via route middleware or make those links non-anchor elements during export (using the X-Laravel-Export header). Do not assume the crawler respects robots.txt.

Query-string paths and filesystem escaping. URL encoding for paths with query parameters was fixed in 1.2.1; if you're pinned to an older 1.x, upgrade before exporting anything like /search?q=.... Even then, prefer clean path segments over query strings — many static hosts treat ? literally and serve a single mangled filename.

mix-manifest.json and PHP files are excluded by default. If your host needs a manifest (or you renamed the Vite manifest), adjust exclude_file_patterns — it matches the source path, so a pattern like /\.php$/ will also strip legitimate .php files you meant to ship.

Symlinks. 1.4.1 fixed a crash on symlinked or non-regular filesystem entries in include_files. If you're on 1.4.0 or earlier and include directories with symlinks (common with storage links), you'll get an exception mid-export; pin ^1.4.1.

Hooks are shell commands run from the app root with no shell abstraction. Absolute paths are used in the README examples for a reason — GUI-launched processes and CI images often have a minimal PATH, so yarn, npm, or netlify may not resolve. A failing hook aborts the export; run --skip-all to confirm whether the export or the hook is at fault.

Streaming changes what's available during export. With use_streaming enabled the crawler consumes responses without buffering whole bodies; anything you need to post-process per page is harder to reach. Only enable it once you've confirmed you actually have a memory problem — measure first.

Exporter injections belong in boot(), not register(). Configuration calls like paths() need the container and often the database; putting them in register() gives you confusing empty or partially configured exports.

Nothing is cached between runs. Every export re-renders every page. For content sites this is fine; for anything with expensive queries on a listing page, consider eager-loading in your controller or rendering with a materialized cache. There's no incremental/diff mode, so CI export times grow linearly with page count — budget for it.

No dependents and modest adoption (698 stars) means fewer battle-tested edge cases. The version history shows steady Laravel-compatibility bumps (through 13.x) but little feature churn; treat undocumented behaviour as something to verify by inspecting the dist output rather than trusting the README. Diff the generated tree against your route list as a smoke test — it catches missing pages, stray index.php, and unbuilt assets in one pass.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mydnic/volet-feature-board-filament-plugin
wmde/email-address
refinery29/test-util
dees040/laravel-api-responses
teamzac/geocoder
ideatica/config-writer
keaton-php/pipe-dream
jacksonwebservices/laravel-user-suite
maksimru/conditional-laravel-debugbar
gismatul/nova-support-chat
gismatul/laravel-support-chat
networkrailbusinesssystems/oracle-api
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
refinery29/php-cs-fixer-config
parsnick/steak