Generate a static website serve by github pages, apaches with one command or via the admin.
composer require pushword/static-generator
That’s it! If you have a custom installation (not used the default installer), you may have a look inside vendor/pushword/admin/install.php.
Add in your current config/packages/pushword.yaml for an App or globally under static_generator: in config/packages/static_generator.yaml.
# In pushword.yaml under your app config:
pushword:
apps:
- host: example.tld
static_generators: [Pushword\StaticGenerator\Generator\PagesGenerator, ...]
static_symlink: true
static_dir: '%kernel.project_dir%/static/{main_host}'
static_assets: ['assets', 'bundles'] # files/folders from public/ to copy
# Or globally in config/packages/static_generator.yaml:
static_generator:
static_generators: apache # shortcuts: apache, github, frankenphp
static_symlink: true
static_dir: '%kernel.project_dir%/static/{main_host}'
The default generators are compatible with Apache/Litespeed and FrankenPHP/Caddy (generating .htaccess and Caddyfile).
static_symlinkControls whether media and assets are symlinked or copied to the static output directory.
| Value | Media | Assets |
|---|---|---|
true (default) | symlink | symlink |
false | copy | copy |
['media'] | symlink | copy |
['assets'] | copy | symlink |
['media', 'assets'] | symlink | symlink |
The most common use case for the array form is ['media']: symlink media files (fast, saves disk space) while copying assets (so they can be deployed independently).
# Symlink media only, copy assets
static_symlink: ['media']
When using GitHub Pages (CNAME generator), copy is forced regardless of this setting.
static_assets (formerly static_copy)List of files or folders in your public/ directory to include in the static output. Default: ['assets', 'bundles'].
static_assets: ['assets', 'bundles']
The old name static_copy still works as a deprecated alias.
static_html_max_ageCache TTL for HTML pages (in seconds). Default: 10800 (3 hours).
static_html_max_age: 86400 # 24 hours
static_html_stale_while_revalidateAdds stale-while-revalidate to the Cache-Control header, allowing CDNs and browsers to serve stale content while revalidating in the background. Set to 0 to disable. Default: 3600 (1 hour).
static_html_stale_while_revalidate: 0 # disabled
Both settings apply to HTML pages only. Static assets (images, JS, CSS, fonts) always use a 1-year TTL.
# Generate all apps
php bin/console pw:static
# Generate 1 app
php bin/console pw:static $host
# (re)Generate only one page
php bin/console pw:static $host $slug
# Only regenerate what changed since the last run
php bin/console pw:static $host --incremental
--incremental skips a page when neither its updatedAt nor the host’s render epoch moved since it was last generated (state lives in var/.static-generation-state.json). The epoch is bumped by anything that can change rendered HTML without touching the page row: snippet, media and template edits, review publications, and edits of other pages that listings render — see the render epoch. So an incremental cron converges on a fully fresh site even for changes no updatedAt reflects. Re-rendered pages whose HTML is byte-identical skip the write, keeping deploy diffs and rsyncs quiet.
Pages deleted or unpublished since the last run are pruned: the in-place build removes their generated files and compression sidecars along with their state entries (the full build gets this for free from its atomic dir swap). Only pager files (slug/2.html) wait for the next full build — their directory also holds child pages’ output.
Bulk flat imports bump the epoch too — the PageCacheSuppressor mutes the per-page messages, never the bump — so pw:flat:sync && pw:static --incremental is a complete publish chain. It is the pushword-deploy publish default.
bin/console cache:clear wipes the epoch storage (a file pool under var/cache/{env}/pw_render_epoch/): the first incremental run after a deploy regenerates everything — which is correct, since a deploy may change templates or code.
The same three scopes are reachable over HTTP once pushword/api is installed, so a script or an agent editing pages remotely can publish its own changes: POST /api/static/{host}/{slug} rebuilds one page synchronously, POST /api/static/{host} starts a background pass and returns a URL to poll. See Static regeneration.
Hosts with 10+ pages are rendered by parallel worker processes. Nothing to configure.
The workers used to be spawned with an opcache file cache (opcache.enable=1 opcache.enable_cli=1 opcache.file_cache=…) so compiled scripts survived their short lives — worth about 18% on a fresh pass. They no longer are: that combination segfaults the worker on some PHP builds, and a worker killed by a signal takes the build with it (Worker N failed (exit 139: Segmentation violation), no output). A build that finishes beats a build that is faster when it survives.
The parent pw:static process (and any sequential build) can still opt into a file cache by hand, if your PHP is not one of the affected builds — keep the validation flag, since the cache outlives composer update:
php -d opcache.enable_cli=1 -d opcache.file_cache=var/cache/opcache -d opcache.validate_timestamps=1 bin/console pw:static
Set cache: static on an app to pre-render pages into public/cache/{host}/ so the web server can serve them directly without booting PHP. Unlike the full static export, the application keeps running and invalidates the cache automatically on page save.
See Page Cache for setup, Caddy config, and the pw:cache:clear command.
You must import the generated static/example.tld/.Caddyfile in your main Caddyfile.
If you still use the default Caddyfile (from pushword/dev-app), see the last commented part:
import static/example.tld/.Caddyfile