symplify/symfony-static-dumper
Symfony Static Dumper converts a Symfony application into a static HTML site. It crawls configured routes, renders pages, and writes them to disk, enabling fast, low-maintenance static deployments while preserving Symfony routing and templates.
symplify/symfony-static-dumper is a dev-only console tool that boots your Symfony kernel, iterates the router's GET routes, renders each one through the HTTP kernel, and writes the responses plus a configured set of public/ directories into a static output/ folder. Day-to-day, it is the "build step" for a brochure site, docs site, or blog — not for anything with a login or a POST.
Minimal setup:
composer require symplify/symfony-static-dumper --dev
// config/bundles.php
return [
// ...
Symplify\SymfonyStaticDumper\SymfonyStaticDumperBundle::class => ['dev' => true],
];
Then:
php bin/console dump-static-website -v
ls output/ # about/index.html, index.html, assets/...
Look first at three things in the vendor directory (the package is deprecated, so the source is the documentation):
src/Command/ — the console command name, options and verbosity output.src/Config/ — the StaticDumperConfig class; this is your extension point for asset directories and route skipping.README.md at the tag you pinned — option names drifted across versions.First use case to prove it works: a two-route app (/ and /about) with no database and no forms. Get output/index.html and output/about/index.html produced, serve output/ with php -S localhost:8000 -t output, and only then add real content. Doing it in that order separates "my config is wrong" from "my app has a hidden dependency."
Wire it as a build artifact, not a runtime concern. Keep the package in require-dev, run the dump in a CI job that has dev dependencies and a seeded database, and publish only output/. The Symfony app itself is not deployed.
# .github/workflows/static.yml (sketch)
- run: composer install
- run: php bin/console assets:install
- run: yarn build # Encore must run BEFORE dumping
- run: php bin/console doctrine:fixtures:load -n
- run: php bin/console dump-static-website -v
- uses: actions/upload-pages-artifact@v3
with: { path: output }
Configure assets by explicit copy, not by magic. The dumper does not run Encore or resolve the manifest; it copies directories you declare. Extend the config class:
namespace App\StaticDumper;
use Symplify\SymfonyStaticDumper\Config\StaticDumperConfig;
final class AppStaticDumperConfig extends StaticDumperConfig
{
public function getStaticDirectories(): array
{
return [
__DIR__ . '/../../public/build' => 'build',
__DIR__ . '/../../public/images' => 'images',
__DIR__ . '/../../public/favicon.ico' => 'favicon.ico',
];
}
public function getRoutesToSkip(): array
{
return ['admin_dashboard', 'contact_submit', 'webhook_stripe'];
}
}
Register it over the package's default (service alias or DI override in config/services.yaml) and confirm with php bin/console debug:container StaticDumperConfig that yours wins.
Skip-list triage as the primary workflow. On a real app, run once, collect every route that errors out or produces nonsense, and move them into getRoutesToSkip(). Typical additions: admin areas, anything requiring a session, error/preview routes, JSON APIs consumed by JS.
Route parameters. Routes with required placeholders generally can't be auto-dumped. Either enumerate the concrete URLs by adding thin static routes, or pre-generate a sitemap-style list and hit those paths. If the package version supports parameter seeds, prefer that over duplicating routes — duplicated routes silently drift from the real ones.
Serve the output with a redirect-aware server config, because the dumper writes path/index.html, not path.html:
location / {
try_files $uri $uri/ $uri/index.html =404;
}
Use it for PR previews. Because it's just a command, a per-PR job that dumps and uploads to a preview URL gives reviewers a real, crawlable site — cheap and surprisingly high-leverage for marketing/CMS work.
deprecated-packages/, has no dependents, and gets no fixes. Pin the exact version ("symplify/symfony-static-dumper": "x.y.z"), vendor-lock it, and treat the alternative (Stenope, or a wget --mirror / Panther-based crawler of a running container) as the exit plan when a PHP or Symfony upgrade breaks it.app.request.host, url() in emails, canonical tags and og:image will all be localhost. Set router.request_context.host / scheme in the dump environment (or use default_uri under framework.router), and prefer relative asset paths in templates.handle() calls in one PHP process means the Doctrine identity map grows and any service caching per-request data (locale, current user, breadcrumbs) can bleed into later pages. If page 40 looks like page 3, that's this. Watch memory and consider chunking the route list across several invocations.{{ 'now'|date }} in a footer, random featured posts, or ad-slot rotation makes every build a 400-file diff. Freeze the clock (ClockMock, a fixed APP_BUILD_TIME env) and sort any "random" collections by a stable key.getRoutesToSkip(), run with -v, and inspect the single file. Much faster than reading a 300-line failure tail. Also assert on status code — a rendered 500 page is still written to disk and looks like success.encore production, assets:install, then the dump. Dumping before building ships build/entrypoints.json referencing hashed files that don't exist yet, and the copy step will silently copy nothing./foo/ while your file is foo/index.html is fine on nginx with try_files and broken on S3 without an index-suffix rule. Test the real host, not php -S.X-Debug-Token comments). Prefer a small decorator over editing vendor code — you will need to upgrade eventually.How can I help you explore Laravel packages today?