Skip to content

[BUG] Site duplication still loses postmeta for nav_menu_item, attachment, elementor_library, and Kit (post-PR #803) — production patch ready to integrate #820

@kenedytorcatt

Description

@kenedytorcatt

TL;DR

PR #803 (merged) fixes Kit URL replacement, but customer purchases at https://kursopro.com (production WP Ultimo multisite, ~250+ subsites) still produce broken clones daily. Customers see wrong colors, missing logos, broken menus, "TU LOGO AQUÍ" placeholders.

I built a 774-line mu-plugin (kp-elementor-css-on-clone.php v2.9) that patches everything on our end. It's been running in production since 2026-04-13 with zero failures. I'd love for this logic to live inside Multisite Ultimate natively so I can retire the patch.

Full code (production-tested): https://gist.github.com/kenedytorcatt/7a0341f56a092466a3a039463a2518c7


The 4 bugs that survive PR #803

After MUCD_Data::copy_data() finishes (regardless of whether it's called via MUCD_Duplicate::duplicate_blog() or via WP Ultimo's class-site-duplicator.php), these postmeta rows are missing in the cloned site:

Bug 1 — Kit _elementor_page_settings not fully copied

Bug 2 — nav_menu_item postmeta not copied

  • Symptom: menus render as empty <li> tags with no titles, no URLs
  • Diagnosis: nav_menu_item POSTS are copied (same IDs preserved) but their postmeta rows are NOT
  • Missing keys: _menu_item_type, _menu_item_object_id, _menu_item_object, _menu_item_url, _menu_item_menu_item_parent, _menu_item_target, _menu_item_classes, _menu_item_xfn

Bug 3 — attachment postmeta not copied

  • Symptom: logos, default icons, Kit images render as broken/missing
  • Missing keys: _wp_attached_file, _wp_attachment_metadata, _wp_attachment_image_alt

Bug 4 — elementor_library CPT postmeta not copied

  • Symptom: custom headers, footers, popups render as skeletons or default placeholder text
  • Real case: clone of "plantilla4" → shopmariana.kursopro.com showed "TU LOGO AQUI" text instead of header logo
  • Diagnosis: plantilla4 has 43 posts with _elementor_data, clone had only 10 (13 elementor_library templates were missing all postmeta)

Critical insight (took me a week to figure out)

WP Ultimo customer purchases do NOT fire mucd_after_copy_data because class-site-duplicator.php calls MUCD_Data::copy_data() directly (not MUCD_Duplicate::duplicate_blog()). Instead, WP Ultimo fires its own wu_duplicate_site hook AFTER duplication.

This means:

  • The correct hook for WP Ultimo flows is wu_duplicate_site (priority must be before your Elementor_Compat::regenerate_css which runs at default 10)
  • mucd_after_copy_data only fires for manual admin duplications
  • Both paths need coverage

Also: the source template ID passed to MUCD's hooks is hardcoded by the duplication call, but the customer's REAL template choice is in get_site_meta($to, 'wu_template_id'). Always prefer the meta over hook params.


How to integrate this into Multisite Ultimate natively

I'm not asking you to copy my mu-plugin verbatim. Here's the minimum integration that would let me retire it:

Step 1 — In class-site-duplicator.php, after MUCD_Data::copy_data() returns, run a postmeta backfill pass:

// Pseudo-code — adapt to your codebase style
$this->backfill_missing_postmeta($from_site_id, $to_site_id, [
    'nav_menu_item' => [
        '_menu_item_type', '_menu_item_object_id', '_menu_item_object',
        '_menu_item_url', '_menu_item_menu_item_parent', '_menu_item_target',
        '_menu_item_classes', '_menu_item_xfn',
    ],
    'attachment' => [
        '_wp_attached_file', '_wp_attachment_metadata', '_wp_attachment_image_alt',
    ],
    'elementor_library' => '_elementor_*', // wildcard match
    'page' => '_elementor_*',
    'post' => '_elementor_*',
]);

$this->backfill_active_kit_settings($from_site_id, $to_site_id);

Step 2 — backfill_active_kit_settings() implementation:

private function backfill_active_kit_settings($from_site_id, $to_site_id) {
    switch_to_blog($from_site_id);
    $kit_id_from = (int) get_option('elementor_active_kit', 3);
    $kit_settings = get_post_meta($kit_id_from, '_elementor_page_settings', true);
    restore_current_blog();

    if (empty($kit_settings)) return;

    switch_to_blog($to_site_id);
    $kit_id_to = (int) get_option('elementor_active_kit', 3);
    // CRITICAL: must be update_post_meta (overwrite) — not INSERT NOT EXISTS
    update_post_meta($kit_id_to, '_elementor_page_settings', $kit_settings);
    delete_post_meta($kit_id_to, '_elementor_css'); // force regen
    if (class_exists('\Elementor\Core\Files\CSS\Post')) {
        (new \Elementor\Core\Files\CSS\Post($kit_id_to))->update();
    }
    restore_current_blog();
}

Step 3 — Add post-clone verification (catches edge cases):

// After backfill, verify the kit settings actually got copied
private function verify_kit_integrity($from_site_id, $to_site_id) {
    // compare bytes of _elementor_page_settings between source and clone
    // if clone < 80% of source bytes -> re-run backfill + schedule retry
}

Step 4 — In class-site-duplicator.php, when resolving the source for cloning:

// Always prefer wu_template_id meta over the explicit param
$meta_template = (int) get_site_meta($to_site_id, 'wu_template_id', true);
if ($meta_template > 0) {
    $from_site_id = $meta_template;
}

This eliminates the hardcoded source bug where MUCD passes a different blog ID than the customer actually selected.


Reference implementation

Full production code in this Gist (774 lines, copy whatever you need):
https://gist.github.com/kenedytorcatt/7a0341f56a092466a3a039463a2518c7

Key functions to look at:

  • kp_run_complete_clone_fix() — universal entry point with double-execution guard
  • kp_fix_kit_postmeta_on_clone() — Bug 1 fix (uses update_post_meta not INSERT)
  • kp_fix_nav_menu_postmeta_on_clone() — Bug 2 fix
  • kp_fix_attachment_postmeta_on_clone() — Bug 3 fix
  • kp_fix_all_elementor_postmeta_on_clone() — Bug 4 fix (catch-all for any _elementor_* meta)
  • kp_verify_and_fix_kit() — post-clone verification + auto-retry at T+15s

Hook setup (triple safety):

add_action('wu_duplicate_site',          'kp_run_complete_clone_fix', 5);     // PRIMARY (WP Ultimo)
add_action('mucd_after_copy_data',       'kp_run_complete_clone_fix', 10, 2); // FALLBACK (manual MUCD)
add_action('wu_pending_site_published',  'kp_run_complete_clone_fix', 9999);  // SAFETY NET

Why this is urgent

We're losing customers. Every broken clone = customer support ticket + refund risk. PR #803 was a great start but doesn't cover the postmeta layer. Once you integrate this, I retire 2 mu-plugins from our stack (the patch + a defensive backup layer).

Happy to test any branch you put up. Can also provide more production logs / screenshots if useful.

Thanks David — your work on Multisite Ultimate is what makes our SaaS possible.

— Kenedy / KursoPro

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions