Transform Pushword in a FlatFile CMS.
composer require pushword/flat
Globally under pushword_flat (in config/packages).
Or for multi-sites in config/packages/pushword.yaml.
pushword_flat:
flat_content_dir: content # default value
# Change detection cache TTL in seconds (default: 300 = 5 minutes)
change_detection_cache_ttl: 300
# Auto-export to flat files after admin modifications (default: true)
auto_export_enabled: true
# Use background process for deferred export (default: true)
use_background_export: true
# Editorial lock TTL in seconds (default: 1800 = 30 minutes)
lock_ttl: 1800
# Auto-lock when flat files are modified (default: true)
auto_lock_on_flat_changes: true
php bin/console pw:flat:sync [host] [options]
Options:
| Option | Description |
|---|---|
host | Optional host to sync (uses default app if not provided) |
--mode, -m | Sync direction: auto (default), import, export |
--entity | Entity type: page, media, conversation, all (default) |
--force, -f | Force overwrite even if files are newer than DB |
--skip-id | Skip adding IDs to markdown files and CSV indexes |
--no-backup | Disable automatic database backup before import |
Examples:
# Auto-detect: imports if flat files are newer, exports if DB is newer
php bin/console pw:flat:sync
# Force import (flat files → database)
php bin/console pw:flat:sync --mode=import
# Force export (database → flat files)
php bin/console pw:flat:sync -m export
# Sync only pages on a specific host
php bin/console pw:flat:sync example.tld --mode=import --entity=page
# Export without adding IDs to files
php bin/console pw:flat:sync --mode=export --skip-id
# Import without creating a database backup
php bin/console pw:flat:sync --mode=import --no-backup
The lock system prevents concurrent modifications between flat files and admin interface, inspired by LibreOffice's locking mechanism.
# Acquire a lock (shows warning in admin)
php bin/console pw:flat:lock [host] [--ttl=1800] [--reason="Editing flat files"]
# Release the lock
php bin/console pw:flat:unlock [host]
How it works:
pw:flat:lock for explicit control during extended editing sessionsWhen both flat files and database are modified since the last sync, a conflict occurs. The system uses a "most recent wins" strategy:
filename~conflict-{id}.md# List and clear conflict backup files
php bin/console pw:flat:conflicts:clear [host] [--dry-run]
Conflict backup files:
page~conflict-abc123.md (contains the losing version with a comment header)index.conflicts.csv (appends conflict details for media/conversation)php bin/console pw:ai-index [host] [exportDir]
Generate two CSV files (pages.csv and medias.csv) with metadata useful for AI tools.
Where:
host is optional (uses default app if not provided)exportDir is optional (uses flat_content_dir by default)pages.csvContains page metadata with the following columns:
slug - Page slugh1 - Page H1 titlecreatedAt - Creation date (Y-m-d H:i:s)tags - Page tagssummary - Page summary/excerptmediaUsed - Comma-separated list of media files used in the pageparentPage - Parent page slug (if any)pageLinked - Comma-separated list of page slugs linked in the contentlength - Content length in charactersmedias.csvContains media metadata with the following columns:
media - Media filenamemimeType - MIME typename - Media nameusedInPages - Comma-separated list of page slugs using this mediaBy default, the content may be organized in content/%main_host%/ dir and image may be in content/%main_host%/media or in media
Eg:
content
content/homepage.md
content/kitchen-skink.md
content/other-example-kitchen-sink.md
content/en/homepage.md
content/en/kitchen-skink.md
content/media/default/illustation.jpg
content/media/default/illustation.jpg.yaml
kitchen-sink.md may contain :
---
h1: 'Welcome in Kitchen Sink'
locale: fr
translations:
- en/kitchen-skink
main_image: illustration.jpg
images:
- illustration.jpg
parent:
- homepage
metaRobots: 'no-index'
name: 'Kitchen Sink'
title: 'Kitchen Sink - best google restult'
#created_at: 'now' # see https://www.php.net/manual/fr/datetime.construct.php
#updated_at: 'now'
---
My Page content Yeah !
Good to know :
.md) and can be override by a property in yaml fronthomepage 's file could be named index.md or homepage.mdcustomPropertiesThe translations property handles the bidirectional many-to-many relationship between pages for internationalization (hreflang).
Key behaviors:
translations property, existing translations in the database are preserved unchanged.translations: [] to explicitly remove all translations from a page.Examples:
# In fr/about.md - adds en/about as translation
---
translations:
- en/about
---
# In en/about.md - no translations key, existing links preserved
---
h1: About Us
---
With this setup, both pages will be linked as translations of each other after sync.
To remove a translation, you must explicitly set an empty array in both files, or remove the translation from one file while the other file doesn't have a translations key (letting the removal propagate).
When uploading media through the admin interface, Pushword automatically:
For images:
For PDFs:
When importing media via flat files, server-side optimizations are skipped. The optimization commands work on Media entities in the database, so you must import first:
# 1. First, import flat files to database (registers media)
php bin/console pw:flat:sync --mode=import
# 2. Then generate image cache (responsive variants + WebP)
php bin/console pw:image:cache
# 3. Optimize images (lossless compression with optipng, jpegoptim, etc.)
php bin/console pw:image:optimize
# 4. Optimize PDFs (requires ghostscript and/or qpdf)
php bin/console pw:pdf:optimize
One-liner:
php bin/console pw:flat:sync -m import && php bin/console pw:image:cache && php bin/console pw:pdf:optimize
Browser-side scaling (1980x1280) only happens via admin upload. For flat file imports, resize images beforehand using ImageMagick:
# Resize all images in media/ to max 1980x1280 (preserves aspect ratio, only shrinks)
find content/media/ -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.webp" \) \
-exec mogrify -resize '1980x1280>' {} \;
Tip: The
>flag means "only shrink larger images, never enlarge smaller ones".