# laravel-export: turning a Laravel app into a static bundle

> spatie/laravel-export crawls a Laravel application and writes an HTML page for every URL it finds, then bundles the public directory alongside them, so a site built with Laravel, Nova, Wink or Filament can be published to static hosting. Everything about the output is controlled through a config file, a disk, hooks that run shell commands, and an injectable Exporter class.

**spatie/laravel-export** — Create a static site bundle from a Laravel app

- Repository: https://github.com/spatie/laravel-export
- Website: https://spatie.be/open-source
- Stars: 678 · Forks: 62
- Language: PHP
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-laravel-export

## The export command crawls your app and writes a folder

The whole package can be demonstrated with one command:

```bash
$ php artisan export
Exporting site...
Files were saved to disk `export`
```

What that does is described in one sentence in the README: Laravel Export will scan your app and create an HTML page from every URL it crawls. The entire public directory also gets added to the bundle, so your assets are in place too.

That gives you a static site built from a dynamic application. The pitch in the README is that you build your blog or site in Laravel with the tools you are used to, then export a static version and upload it anywhere for hosting, with no need to manage a full-blown server anymore.

The use case it names is writing a site in Laravel with Nova, Wink, Filament, Sharp or any other admin panel, managing the content locally or on a remote server, and then publishing the result to a service like Netlify. The stated benefit is that you keep a dynamic backend of some sort while getting the properties of a static site, which the README lists as speed, simple hosting and scalability.

The trade is the one that comes with any static export. The bundle is a snapshot. A page whose content depends on the request, on a session, or on a database query at render time is exported as whatever the crawler saw, and the crawler is walking the site as an anonymous visitor would see it. That is why the configuration options are so heavily weighted towards choosing exactly which paths get exported.

## Paths, included files and the patterns that keep PHP out

The configuration file lives at config/export.php, and the README says plainly that Laravel Export does not require configuration to get started, but there are a few things you can tweak.

The paths option is an array of URL paths that will be exported to HTML, used to manually determine which pages should be exported:

```php
return [
    'paths' => [
        '/',
        '/rss.xml',
    ],
];
```

The include_files option specifies files and folders relative to the application root that should be added to the export, and by default the entire public folder is included:

```php
return [
    'include_files' => [
        'public' => '',
    ],
];
```

The exclude_file_patterns option checks all source paths of included files and excludes them if they match a pattern. The defaults exclude all PHP files, mainly to stop index.php from appearing in the export, and also exclude mix-manifest.json because it is no longer needed after compilation:

```php
return [
    'exclude_file_patterns' => [
        '/\.php$/',
        '/mix-manifest\.json$/',
    ],
];
```

That default is a nice piece of engineering judgement. Leaving index.php in a static bundle is harmless on paper but it means you have shipped a PHP entry point to a host that may or may not execute it, and shipping the compiled asset manifest is a small leak of build information. Neither is a big deal, but both are exactly the sort of thing you would forget if the package did not think of it for you.

The crawl option is the master switch for the automatic behaviour, and use_streaming reduces memory usage when crawling large sites, which only makes sense when crawl is enabled:

```php
return [
    'crawl' => true,
];
```

```php
return [
    'use_streaming' => true,
];
```

## Every option is also reachable from the Exporter class

Configuration through code is the part that makes this package usable in a script rather than only in a config file. All configuration options that affect the export contents are exposed in the Exporter class, which you can inject to modify the export settings through code:

```php
use Illuminate\Support\ServiceProvider;
use Spatie\Export\Exporter;

class AppServiceProvider extends ServiceProvider
{
    public function boot(Exporter $exporter)
    {
        $exporter->crawl(false);
        $exporter->useStreaming(true);

        $exporter->paths(['', 'about', 'contact', 'posts']);
        $exporter->paths(Post::all()->pluck('slug'));
    }
}
```

Look closely at the last two lines. paths is called twice, first with a literal list and then with the slugs pulled from the database, which means the set of exported URLs is derived from your content rather than hard-coded. That is the difference between an export that goes stale and one that stays correct, and it is only possible because the paths option is exposed as a method rather than locked in a config file.

Injecting into a service provider's boot method is also the point at which the exporter is available with the application booted, so database access works. The cost is that a paths list built from a query is a snapshot taken at export time, which is exactly the behaviour you want for a static bundle and exactly the behaviour you do not want if a page is meant to be live.

The same class also carries the crawl and useStreaming switches as methods, so the config file and the code path are two ways of setting the same state, and the code path wins when both are present.

## Disks decide where the bundle lands, and S3 is one option

The default disk configuration is the smallest possible answer:

```php
// config/export.php

return [
    'disk' => 'export',
];
```

The README explains the point of that indirection: because it is a filesystem disk, you can use other filesystem drivers, so you could export your site straight to something like S3. The export and the deployment become one step rather than two.

The disk itself is defined in config/filesystem.php, and the example shows a local disk rooted at a folder in the application root:

```php
// config/filesystem.php

return [
    'disks' => [
        //

        'export' => [
            'driver' => 'local',
            'root' => base_path('out'),
        ],
    ],
];
```

The README also notes that by default the static bundle is saved in a dist folder in your application root, so if you want a different folder you configure a new disk. The example above is exactly that case, naming the disk export and pointing its root at a folder called out.

One thing to be careful about when you point this at object storage: the export writes files, and whether the driver supports the write pattern the exporter uses is worth checking against your driver rather than assuming. A local disk always works, and the README is written around the local case.

## Hooks are shell commands, and skipping them is a flag

The hooks feature is what turns a build step into a deployment step. The before and after hooks allow you to do things before or after an export, and hooks can contain any shell command. The default configuration has no hooks configured, but the README shows two examples.

The before hook builds assets with Yarn before every export:

```php
return [
    'before' => [
        'assets' => '/usr/local/bin/yarn production',
    ],
];
```

The after hook deploys the static bundle to Netlify with their CLI tool after the export:

```php
return [
    'after' => [
        'deploy' => '/usr/local/bin/netlify deploy --prod',
    ],
];
```

The hooks are named, and the naming is what gives you the skip flags. To run an export without certain hooks you use --skip-{hook} flags, so the deploy example above is skipped with:

```bash
php artisan export --skip-deploy
```

And there are flags to skip whole classes of hook:

```bash
php artisan export --skip-before
```

```bash
php artisan export --skip-after
```

```bash
php artisan export --skip-all
```

The absolute paths in the examples are worth copying rather than the relative ones. A hook that runs from a different working directory than you expect will fail in a way that is annoying to debug, and a shell command with no shell involved, such as a bare npm run, may not resolve the way you think it will.

The hooks are also a security consideration if you accept a config file from somewhere untrusted, since they execute arbitrary shell commands by design. In a normal project the config file is yours, so this is only worth thinking about if you build export configurations dynamically.

## What the package does not do, and where it stands

The limits of this package are the limits of static export, and they are worth being explicit about.

There is no incremental or partial export. The command crawls and writes everything, so a large site means a long run, and the memory question is answered by the use_streaming option rather than by anything cleverer. There is no build cache and no content hashing beyond what the asset pipeline already does, because the export copies the public directory rather than compiling anything itself. The asset compilation is the job of your before hook.

The paths option is the practical control surface, and it is a whitelist rather than a blacklist. Anything you did not name and the crawler did not reach is not in the bundle. A page behind a login, a search results page, a cart, anything whose content is per-visitor, is either missing or frozen at whatever the crawler saw. There is no per-page template handling and no concept of a layout that varies by route.

The package itself is a small, focused release with an ordinary maintenance profile. The versions are 1.5.0 from 2026-06-26, 1.4.1 from 2026-05-26 and 1.4.0 from 2026-04-28, and the last push was on 2026-08-07. The licence is MIT with a LICENSE.md file at the root. The repository carries a CHANGELOG.md, an UPGRADING.md, a config/ directory, src/, tests/ and a phpunit.xml.dist, so there is a test suite and a documented upgrade path, which is what you want from a package that writes into your deployment.

The README also says that if you find a security bug you should mail the address given rather than using the issue tracker, which is standard practice but a reminder that the package is run with the privileges of your Laravel application.

## Conclusion

Adopt laravel-export if you already build your site with Laravel and want to publish it as static files, either to remove the need to run a PHP server or to keep an admin panel on a dynamic backend while the public site is served from object storage or a CDN. Do not adopt it for a site whose URLs are not enumerable ahead of time, because the export works by crawling and by an explicit path list, and a page that only exists after a user action will not be in the bundle. Before you commit, run php artisan export once and open the output, check that the crawl has not swallowed routes you meant to keep dynamic, and read UPGRADING.md since the package is past version 1.5 and has had breaking changes between releases.

## FAQ

### How do I install and run spatie/laravel-export?

Install it with composer require spatie/laravel-export, optionally publish the config file with php artisan vendor:publish --provider=Spatie\Export\ExportServiceProvider, and run php artisan export. The command prints that it is exporting and then reports which disk the files were saved to.

### How do I control which pages laravel-export includes?

By default it crawls the site and exports every page it finds, and you can disable that with the crawl option. The paths option is an array of URL paths exported to HTML, and the same option is available as a method on the Exporter class so you can build the list from a query such as Post::all()->pluck('slug').

### What are the before and after hooks in laravel-export for?

They are named shell commands that run before or after an export. The README's examples build assets with Yarn in a before hook and deploy to Netlify with their CLI in an after hook, and you can skip a specific hook with a --skip-{hook} flag or all of them with --skip-all.

### Can laravel-export write the site directly to S3?

Yes. The config file has a disk option, and the README says that because it is a filesystem disk you can use other drivers so the site can be exported straight to something like S3. By default the bundle is saved in a dist folder in the application root, and a different folder is configured by defining a new disk in config/filesystem.php.

### Why does laravel-export exclude PHP files and mix-manifest.json by default?

The default exclude_file_patterns remove all PHP files, mainly to stop index.php from appearing in the export, and also remove mix-manifest.json because it is no longer needed after compilation. The patterns are regular expressions matched against the source paths of included files.

## Sources

- [License: MIT](https://github.com/spatie/laravel-export/blob/main/LICENSE)
- [Project website](https://spatie.be/open-source)
- [README](https://github.com/spatie/laravel-export/blob/main/README.md)
- [Releases](https://github.com/spatie/laravel-export/releases)
- [spatie/laravel-export on GitHub](https://github.com/spatie/laravel-export)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-laravel-export
