Multisite content and media copy
Bulk actions that copy pages (with their ACF flexible content and images) and media library items from one subsite to another. Added because the country sites share most of their page content.
Why it exists
Section titled “Why it exists”The UK site is the content source; Ireland, USA and Australia reuse a large part of it with local edits. Without this, launching a new country site means rebuilding every page by hand, and ACF flexible content does not survive a copy/paste of post content because the layout data lives in post meta.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/supplychainschool/inc/multisite-functions.php:17 | Registers “Copy to {site}” bulk actions on the Pages list |
wp-content/themes/supplychainschool/inc/multisite-functions.php:40 | stq_bulk_action_multisite_handler() — the page copier |
wp-content/themes/supplychainschool/inc/multisite-functions.php:284 | get_images_from_blocks() — finds images inside blocks |
wp-content/themes/supplychainschool/inc/multisite-functions.php:337 | stq_copy_attachment_to_blog() |
wp-content/themes/supplychainschool/inc/multisite-functions.php:394 | Registers “Copy to {site}” bulk actions on the Media library |
wp-content/themes/supplychainschool/inc/multisite-functions.php:2 | Makes the parent theme’s acf-json load in child themes |
Also relevant: the multisite-clone-duplicator and post-duplicator
plugins are installed for whole-site and single-post duplication.
How it works
Section titled “How it works”Page copy
Section titled “Page copy”For each selected page, per target blog (multisite-functions.php:51):
- Read the source post and
parse_blocks()its content. - Read the ACF flexible content by field key:
get_field('field_5c40bad9dd95f', $post_id)(:57) — a hard-coded key. - Read the raw
post_contentstraight from the database ($wpdb->get_var(...),:81) as well as throughget_post(). - Collect images referenced inside blocks via
get_images_from_blocks(). - Collect the thumbnail and all other post meta.
switch_to_blog($blog_id).acf_parse_save_blocks()the content, thenwp_insert_post().- Overwrite
post_contentdirectly in the database with the raw source value ($wpdb->update,:133) — the comment at:126explains that inserting blocks with HTML content otherwise gets escaped. - Sideload each referenced image with
wp_upload_bits()+wp_insert_attachment()+wp_generate_attachment_metadata(), andstr_replace()the old attachment id for the new one in the content. - Restore the thumbnail: reuse an existing attachment with the same
post_nameif one exists, else sideload it. - Copy every other post meta key verbatim, then
update_field('flexible_content', $acf_content, $inserted_post_id). restore_current_blog().
An admin notice reports the count (:259).
Media copy
Section titled “Media copy”stq_copy_attachment_to_blog() (:337) is simpler: read the original file
path (preferring wp_get_original_image_path()), switch blog, pick a unique
filename, copy() the file into the target uploads directory, insert the
attachment and regenerate metadata.
The Media bulk action is only registered on blog 1 — the guard at :397
returns early when get_current_blog_id() > 1, so you can copy media from
the UK site only.
Image discovery
Section titled “Image discovery”get_images_from_blocks() (:284) walks parsed blocks and looks at
$block['attrs']['data'] for exactly five keys:
background_image, image, images, profile_image, featured_image
Values may be a single id or an array. Anything else is invisible to the copier.
Configuration
Section titled “Configuration”- Multisite must be enabled (
MULTISITE,SUBDOMAIN_INSTALLinwp-config.php). - Site list is capped at
'number' => 50in both bulk-action registrations (:25,:404). - ACF Pro required —
acf_parse_save_blocks()andupdate_field()are ACF functions.
Invariants and gotchas
Section titled “Invariants and gotchas”- The ACF field key
field_5c40bad9dd95fis hard-coded (:57). If the flexible content field is ever recreated it gets a new key and the copier silently copies no flexible content — the page arrives empty of blocks while looking like it succeeded. get_images_from_blocks()reads$block['attrs']['data']without checking it exists (:297). A core Gutenberg block, or any block without ACF data, raises a PHP notice/warning on every copy.- Only five image field names are recognised. A new ACF image field with any other name copies as a stale attachment id pointing at the source blog’s media library — the image renders on the source site and breaks on the target. This is the most likely cause of “the copy is missing images”.
- The id replacement operates on
$item->post_contentand assigns to$post_data['post_content'](:161), but step 8 has already written the raw content directly to the database and$post_datais not re-saved afterwards. The remapped ids are computed and then discarded, so block image references keep the source blog’s ids. - Every other post meta is copied verbatim (
:232), including any meta holding attachment ids or absolute source-site URLs. $post_idis interpolated straight into SQL at:81. It comes from WordPress’s own bulk-action$object_ids, so in practice it is an int, but it is not prepared.- The bulk action label says “Copy”, and it is a copy — despite the action
name prefix
move_to_, the function namestq_multisite_move_media, theerror_log('Moving media to blog ID: …')at:426, and the admin notice reading “%d posts have been moved” (:271). Nothing is deleted from the source. - The media notice hard-codes the destination name as “Store”
(
:444) — copied from another project. - No capability check beyond WordPress’s own bulk-action handling, and no confirmation step. Selecting 50 pages and picking a target starts 50 copies with image sideloading inline in one request.
Changing it safely
Section titled “Changing it safely”- Adding an image field to a block? Add its field name to
$keys_to_checkinget_images_from_blocks()(:299) in the same commit, or media will not follow the copy. This is the single most important coupling in this feature — see [[acf-flexible-content-blocks]]. - If the flexible content field is rebuilt, update the hard-coded key at
:57. Better: replace it with the field name ('flexible_content'), which is what the write side already uses at:240. - New post types to copy: the bulk action is registered against
bulk_actions-edit-pageonly. Registerbulk_actions-edit-{type}and reuse the same handler — it already branches onpost_type != 'attachment'. - Verify by hand on a staging network: copy one page with a hero background
image, a gallery and a featured image, then check on the target site that
all three render and that the attachment ids in
post_contentpoint at the target blog’s media library. - Deliberately not abstracted: the direct
$wpdb->update()ofpost_content.wp_insert_post()escapes serialised block markup, which breaks ACF blocks; the comment at:126records this.
None. This feature writes across blogs and has no dry-run mode — test on staging only.
Related: [[acf-flexible-content-blocks]], [[site-chrome-and-navigation]].