Pushword runs several operations as background tasks to avoid blocking HTTP requests:
pw:image:cache)pw:image:optimize)pw:pdf:optimize)pw:static)pw:page-scan)pw:flat:sync)Two modes are available, configured via background_task_handler:
Uses nohup to spawn background processes with PID-based locking. Works out of the box with no extra setup.
# config/packages/pushword.yaml
pushword:
background_task_handler: process
Dispatches tasks onto the Symfony Messenger bus. Better suited for high-load sites or environments where direct process spawning is restricted.
# config/packages/pushword.yaml
pushword:
background_task_handler: messenger
composer require symfony/messenger
config/packages/messenger.yaml:framework:
messenger:
transports:
async:
dsn: 'doctrine://default' # uses your existing database — no Redis needed
routing:
'Pushword\Core\BackgroundTask\RunCommandMessage': async
For higher throughput, you can use Redis (
redis://localhost:6379/messages) or RabbitMQ instead of the Doctrine transport.
php bin/console messenger:consume async
The HTMX-based admin polling UI works identically in both modes since existing commands handle their own PID registration and output writing.
Commands like pw:image:cache use per-file locks to prevent duplicate processing of the same image. When a second instance is triggered for the same file while the first is still running, it skips silently.
Each upload spawns a separate background process. Different files can run concurrently without blocking each other (per-file locks). Under heavy concurrent load, many simultaneous processes may be spawned.
With Messenger, tasks are queued and processed sequentially by the worker. Even if many uploads happen simultaneously, all cache generation tasks are dispatched to the message bus. The worker processes them one at a time, ensuring controlled resource usage.
# config/packages/pushword.yaml
pushword:
background_task_handler: messenger
Configure commands that run automatically on specific triggers: when a scheduled page becomes published, or on a cron schedule.
# config/packages/pushword.yaml
pushword:
scheduled_commands:
- { command: 'pw:static -i', on: 'publish' }
- { command: 'pw:static', on: 'cron: 0 4 * * *' }
Triggers:
publish — runs when a page with a future publishedAt date becomes publishedcron: <expression> — runs on a cron schedule (e.g., cron: 0 4 * * * for daily at 4am)Everything is automatic — no system cron needed. Install the Scheduler component:
composer require symfony/scheduler
Then consume the Pushword schedule alongside your async transport:
php bin/console messenger:consume async scheduler_pushword
For on: publish triggers, set up a system cron to run pw:cron periodically:
*/5 * * * * cd /path/to/project && php bin/console pw:cron
For on: cron: triggers in process mode, you need to set up the corresponding system crons manually.
These commands are run manually to maintain media consistency:
pw:media:normalize-filenames)Renames media files to URL-safe slugs. Use --dry-run to preview changes.
php bin/console pw:media:normalize-filenames --dry-run
php bin/console pw:media:normalize-filenames
pw:media:clean-duplicates)Detects media entries that share the same file content (SHA-1 hash) and merges them. For each duplicate group, the oldest entry (lowest ID) is kept as canonical. Duplicate filenames are added to the canonical entry's fileNameHistory so existing references keep resolving transparently.
Page mainImage references are transferred to the canonical entry. Physical files and image cache for removed duplicates are cleaned up automatically.
# Preview which media would be merged
php bin/console pw:media:clean-duplicates --dry-run
# Merge duplicates
php bin/console pw:media:clean-duplicates