Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/wira/fylgja-wp-sync/archive/refs/heads/main.zipMaster/slave WordPress content synchronization with WPML translation support.
Requirements
- WordPress 6.0+
- PHP 8.0+
- WPML 4.6+ on both master and slave (matched major.minor version)
- WPML String Translation enabled on both sites for string sync
- WPML's doc translation method may be ATE or Classic — both are supported
Installation
- Drop into
wp-content/plugins/fylgja-wp-sync/on both master and slave (or symlink it). - Activate on both sites.
- Configure each site under Settings → Fylgja Sync:
- Role:
masterorslave. - Remote URL: the other site's URL.
- API key: generate on slave first, paste into master.
- Role:
Configuration
Slave mode
fylgja_slave_mode toggles the slave between two behaviors:
active(default): incoming payloads are applied to local posts/terms/strings.inspect: payloads are received and logged, but no writes happen. Use this to verify correctness before flipping toactive. Both master and slave admin pages display a yellow banner while inspect is on.
Resync All
The master settings page includes a Resync All button. Clicking it pushes every
eligible object (terms → posts → strings) to the slave once. The work runs in the
background on a self-rescheduling 60-second cron tick (fylgja_resync_tick), batching
200 objects per tick. Progress is shown live; the operation can be cancelled and resumed.
Resync is idempotent — running it twice produces the same slave state, because every
object resolves through the same lookup paths used by live sync
(_fylgja_source_id for posts/terms, MD5(context+name+gettext_context) for strings).
Recommended backfill workflow:
- Flip slave to
inspect. - On master, click "Resync All".
- Watch slave's Sync Log tab. Confirm language codes, sibling lookups, and deferred refs look right.
- Flip slave to
active. - Click "Resync All" on master again. Existing slave posts now get their WPML language details attached.
Payload reference
All payloads include "payload_version": 2. The receiver rejects payloads of unknown
versions with 400.
Post / attachment
{
"payload_version": 2,
"source_id": 6860,
"post_title": "Sample Project",
"post_content": "...",
"post_status": "publish",
"post_type": "project",
"post_name": "sample-project",
"post_excerpt": "",
"menu_order": 0,
"meta": { "_thumbnail_id": "123", "...": "..." },
"terms": { "category": [12, 17], "post_tag": [55] },
"wpml": {
"language_code": "en",
"source_trid": 6860,
"source_default_element_id": 6860,
"element_type": "post_project"
}
}
The terms block sends source-side term IDs (integer arrays). The slave resolves them via
_fylgja_source_id term-meta. Unresolvable terms are parked in fylgja_deferred_refs and
resolved later by the deferred sweep (see below).
Term
{
"payload_version": 2,
"source_id": 16,
"taxonomy": "category",
"name": "Architecture",
"slug": "architecture",
"description": "",
"parent_source_id": 0,
"meta": { "...": "..." },
"wpml": {
"language_code": "de",
"source_trid": 7668,
"source_default_element_id": 1,
"element_type": "tax_category"
}
}
String
{
"payload_version": 2,
"source_id": 1490,
"context": "theme_subtitle",
"name": "70c4fe802624c6daef688eced9f59cbc",
"gettext_context": "",
"value": "Categoria",
"status": 0,
"string_type": 0,
"wrap_tag": "",
"translations": {
"es": { "value": "Categoría", "status": 10 },
"de": { "value": "Kategorie", "status": 10 }
}
}
Identity match on the slave is MD5(context + name + gettext_context), compared against
wp_icl_strings.domain_name_context_md5. Translations are written with
ON DUPLICATE KEY UPDATE.
Deferred references
When a post arrives referencing a term (or a menu item referencing a post) that hasn't
synced yet, the slave parks the dependency in wp_fylgja_deferred_refs instead of dropping
it. These rows are resolved by:
- the sweep that runs at the end of every successful apply, and
- a standalone slave-side cron (
fylgja_sweep_deferred, every 5 minutes).
Because of the standalone cron, deferred refs resolve within ~5 minutes even if no further
syncs arrive. Inserts are idempotent on the tuple
(ref_type, dependent_local_id, ref_object_type, ref_source_id), so re-syncing the same
post never produces duplicate pending rows.
Limitations
- Untranslated source strings are intentionally not pushed. Both live sync and Resync push only strings that carry at least one translation. Untranslated source strings self-register on the slave from the same theme/plugin code, so syncing them would only flood the queue with noise.
- String packages (
wp_icl_strings.string_package_id IS NOT NULL) are not synced. Use WPML's package APIs separately. - WPML configuration (active languages, translatable post types, custom field translation rules) is not synced. Both sites must be configured identically.
- WPML Media Translation is not supported. Attachments flow through as language-neutral and are tagged to the default language.
- Bidirectional sync is not supported. Master → slave only.
- Orphan cleanup is operator-driven. If an object is deleted on master before sync caught up, the slave may retain it.
- Homepage grid placement is site-local. The
positionandmeta-emptypost-meta keys (which control a post's anySCALE homepage grid slot) are seeded from the master only on a post's first sync, then never overwritten. This lets the slave curate its own homepage independently; the trade-off is that the slave will not track later placement changes made on the master.
WPML setup checklist
Both sites must agree on:
- Active languages (Settings → WPML → Languages).
- Default language.
- Translatable post types (Settings → WPML → Translation Management → Multilingual Content Setup).
- Custom field translation rules per post type.
A mismatch on any of these will produce silent oddities (e.g., a translation arriving for a language the slave doesn't know about). The sync log preview surfaces these as warnings; check it after the first few backfill ticks.
Upgrading
This plugin uses payload_version to gate compatibility. Upgrading across a major payload
version is a flag-day operation:
- On master, click Sync Now and confirm
wp_fylgja_sync_queuehas no pending rows. - Upgrade the slave plugin first.
- Upgrade the master plugin.
- Flip slave to
inspectand verify a few payloads before flipping back toactive.
Note: the queue's
source_kindcolumn and thefylgja_resync_stateoption are picked up bydbDeltaon activation. After upgrading an existing install, re-activate the plugin so the schema change is applied.
Troubleshooting
- Yellow banner on admin pages: slave is in inspect mode. Flip to active when ready.
- Queue rows piling up: check master's PHP error log for HTTP errors from
wp_remote_post. Common cause: API key mismatch. - Deferred refs not draining: a term or post the slave depends on hasn't arrived yet.
They resolve automatically within ~5 minutes via
fylgja_sweep_deferred; Resync All resolves the rest. - Sync log shows
payload_versionerrors: master and slave are on different plugin versions. Match versions.
License
GPL-2.0-or-later. See LICENSE for the full text.