@@ -261,12 +261,20 @@ protected static function process_duplication($args) {
261261 * customer's real choice in the wu_template_id site meta key.
262262 * Prefer that over the explicit param when available.
263263 *
264+ * Intentionally kept in a separate variable: copy_data() and
265+ * copy_files() have already run with $args->from_site_id. Mutating
266+ * that property would cause copy_users() and downstream callers to
267+ * reference a different source than the one whose data was copied,
268+ * creating an inconsistent clone. Use $template_site_id only for the
269+ * post-copy backfill, integrity check, and action payload.
270+ *
264271 * @since 2.3.1
265272 * @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/820
266273 */
267- $ meta_template = (int ) get_site_meta ($ args ->to_site_id , 'wu_template_id ' , true );
268- if ($ meta_template > 0 && $ meta_template !== (int ) $ args ->from_site_id ) {
269- $ args ->from_site_id = $ meta_template ;
274+ $ template_site_id = (int ) $ args ->from_site_id ;
275+ $ meta_template = (int ) get_site_meta ($ args ->to_site_id , 'wu_template_id ' , true );
276+ if (0 < $ meta_template && $ meta_template !== (int ) $ args ->from_site_id ) {
277+ $ template_site_id = $ meta_template ;
270278 }
271279
272280 /*
@@ -281,7 +289,19 @@ protected static function process_duplication($args) {
281289 * @since 2.3.1
282290 * @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/820
283291 */
284- self ::backfill_postmeta ($ args ->from_site_id , $ args ->to_site_id );
292+ self ::backfill_postmeta ($ template_site_id , $ args ->to_site_id );
293+
294+ /*
295+ * Rewrite source URLs to target URLs in backfilled postmeta rows.
296+ *
297+ * backfill_postmeta() inserts rows after MUCD_Data::copy_data() has
298+ * already run its source→target URL replacement pass, so those rows
299+ * contain raw template URLs. Apply the same replacement here.
300+ *
301+ * @since 2.3.2
302+ * @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/834
303+ */
304+ self ::rewrite_backfilled_postmeta_urls ($ template_site_id , $ args ->to_site_id );
285305
286306 /*
287307 * Verify Kit integrity after backfill.
@@ -293,7 +313,7 @@ protected static function process_duplication($args) {
293313 * @since 2.3.1
294314 * @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/820
295315 */
296- self ::verify_kit_integrity ($ args -> from_site_id , $ args ->to_site_id );
316+ self ::verify_kit_integrity ($ template_site_id , $ args ->to_site_id );
297317
298318 if ($ args ->keep_users ) {
299319 \MUCD_Duplicate::copy_users ($ args ->from_site_id , $ args ->to_site_id );
@@ -316,7 +336,7 @@ protected static function process_duplication($args) {
316336 do_action (
317337 'wu_duplicate_site ' ,
318338 [
319- 'from_site_id ' => $ args -> from_site_id ,
339+ 'from_site_id ' => $ template_site_id ,
320340 'site_id ' => $ args ->to_site_id ,
321341 ]
322342 );
@@ -384,6 +404,76 @@ protected static function backfill_postmeta($from_site_id, $to_site_id) {
384404 self ::backfill_kit_settings ($ from_site_id , $ to_site_id );
385405 }
386406
407+ /**
408+ * Rewrite source-site URLs to target-site URLs in backfilled postmeta rows.
409+ *
410+ * backfill_postmeta() inserts rows after MUCD_Data::copy_data() has already
411+ * run its source→target URL replacement pass (db_update_data()), so those
412+ * rows contain raw template-site URLs. This method applies the same URL
413+ * substitution to the target's postmeta table, correcting any template
414+ * references left by the backfill (e.g. _menu_item_url custom links,
415+ * _elementor_* JSON containing the template domain).
416+ *
417+ * Safe to run after MUCD has already rewritten the copied rows: those rows
418+ * no longer contain the source URL, so REPLACE() is a no-op for them.
419+ *
420+ * @since 2.3.2
421+ * @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/834
422+ *
423+ * @param int $from_site_id Source (template) blog ID.
424+ * @param int $to_site_id Target (cloned) blog ID.
425+ */
426+ protected static function rewrite_backfilled_postmeta_urls ($ from_site_id , $ to_site_id ) {
427+
428+ global $ wpdb ;
429+
430+ $ from_site_id = (int ) $ from_site_id ;
431+ $ to_site_id = (int ) $ to_site_id ;
432+
433+ if ( ! $ from_site_id || ! $ to_site_id || $ from_site_id === $ to_site_id ) {
434+ return ;
435+ }
436+
437+ $ from_blog_url = get_blog_option ($ from_site_id , 'siteurl ' );
438+ $ to_blog_url = get_blog_option ($ to_site_id , 'siteurl ' );
439+
440+ $ from_clean = wu_replace_scheme ((string ) $ from_blog_url );
441+ $ to_clean = wu_replace_scheme ((string ) $ to_blog_url );
442+
443+ if ($ from_clean === $ to_clean ) {
444+ return ;
445+ }
446+
447+ $ to_prefix = $ wpdb ->get_blog_prefix ($ to_site_id );
448+
449+ /*
450+ * Mirror MUCD's two-pass approach: plain URL replacement and a
451+ * JSON-escaped variant (forward slashes encoded as \/).
452+ */
453+ $ replacements = [
454+ $ from_clean => $ to_clean ,
455+ str_replace ('/ ' , '\\/ ' , $ from_clean ) => str_replace ('/ ' , '\\/ ' , $ to_clean ),
456+ ];
457+
458+ // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
459+ foreach ($ replacements as $ from => $ to ) {
460+
461+ if ($ from === $ to ) {
462+ continue ;
463+ }
464+
465+ $ wpdb ->query (
466+ $ wpdb ->prepare (
467+ "UPDATE {$ to_prefix }postmeta SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_value LIKE %s " ,
468+ $ from ,
469+ $ to ,
470+ '% ' . $ wpdb ->esc_like ($ from ) . '% '
471+ )
472+ );
473+ }
474+ // phpcs:enable
475+ }
476+
387477 /**
388478 * Backfill nav_menu_item postmeta from template to cloned site.
389479 *
0 commit comments