Skip to content

Conversation

@ndiego
Copy link
Contributor

@ndiego ndiego commented Nov 20, 2025

Related issues

Proposed Changes

This PR adds the ability to copy (clone, duplicate, etc.) an existing Studio site from both the context menu and from the "More options" menu on the Settings tab. Users have granular control over what gets copied (database, plugins, themes, uploads). Users can also configure different PHP/WordPress versions (and all the other normal settings) for the copied site.

Features

  • Copy site menu option in site settings dropdown and context menu
  • Copy wizard with configurable options:
    • Database (content, settings, users)
    • Plugins
    • Themes
    • Uploads (media library)
    • Site name, path, PHP/WP version overrides
    • Custom domain and HTTPS settings
  • Real-time progress tracking during copy operation
Context menu Settings
image image
Copy screen
image

Implementation

Frontend:

  • New components: copy-site.tsx (menu trigger) and modules/add-site/components/copy-site.tsx (form)
  • useCloneSite() hook for managing clone state
  • Extended useSiteDetails hook with copySite() method
  • Integrated into add-site stepper with /copy route
  • Progress updates via copySiteProgress IPC events

Backend (copySite handler in ipc-handlers.ts:308-565):

  1. Validates source site and stops it if running (prevents DB corruption)
  2. Creates destination directory structure
  3. Copies WordPress core OR installs different WP version if requested
  4. Always copies mu-plugins (SQLite integration)
  5. Conditionally copies plugins/, themes/, uploads/, database/ based on options
  6. If database copied: Updates URLs via WP-CLI
  7. If no database: Fresh WordPress installation
  8. Registers new site, starts it, generates thumbnail
  9. Restarts source site

Telemetry: Added SITE_COPIED metric

How Copy Site Works

  1. User clicks "Copy site" from settings menu or context menu
  2. Copy wizard opens with source site name pre-filled
  3. User selects what to copy (all checked by default), customizes name/path/versions
  4. Click "Copy site" → progress bar shows current step
  5. New site appears in sidebar and starts automatically

Key features:

  • WordPress version flexibility (can copy to different WP version)
  • Automatic URL updates when copying database
  • Safe operation: stops source during copy, restarts after
  • Cleanup on failure: trashes partial copy, restarts source site
  • SQLite integration always copied for database compatibility

Testing Instructions

  • Copy with all options enabled
  • Copy with selective options (e.g., only plugins + themes)
  • Copy with different WordPress/PHP versions
  • Copy without database (fresh install)
  • Copy running site (verify restart)
  • Copy to existing directory (should fail)
  • Verify URL updates in copied database
  • Verify thumbnails generate correctly

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@ndiego ndiego self-assigned this Nov 20, 2025
@ndiego ndiego requested review from bcotrim and sejas November 20, 2025 19:27
@ndiego ndiego marked this pull request as ready for review November 20, 2025 20:33
@github-actions
Copy link
Contributor

📊 Performance Test Results

Comparing 60ddd15 vs trunk

site-editor

Metric trunk 60ddd15 Diff Change
load 13352.00 ms 7608.00 ms -5744.00 ms 🟢 -43.0%

site-startup

Metric trunk 60ddd15 Diff Change
siteCreation 133252.00 ms 132835.00 ms -417.00 ms 🟢 -0.3%
siteStartup 44249.00 ms 52194.00 ms +7945.00 ms 🔴 18.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change

Copy link
Contributor

@bcotrim bcotrim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good and working great!
Thanks @ndiego 👍

I added some comments, mostly regarding code best practices and structure, but nothing major.

Comment on lines +390 to +393
setAddingSiteIds( ( prev ) => {
prev.push( newSite.id );
return prev;
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
setAddingSiteIds( ( prev ) => {
prev.push( newSite.id );
return prev;
} );
setAddingSiteIds( ( prev ) => [ ...prev, newSite.id ] );

<MenuItem
aria-disabled={ isCopyDisabled }
onClick={ () => {
if ( isCopyDisabled || ! selectedSite ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isCopyDisabled is defined as ! selectedSite (line 15), so this condition checks the same thing twice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR, but I noticed the DeleteSite component has a similar issue.
It would be a nice time to address it in my opinion.

import { moreVertical } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { PropsWithChildren } from 'react';
import CopySite from 'src/components/copy-site';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move CopySite and DeleteSite to src/modules/site-settings.
AFAIK these aren't being used anywhere else and src/components should be used for generic, reusable components.

fs.mkdirSync( newPath, { recursive: true } );
}

if ( ! ( await isEmptyDir( newPath ) ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good validation, but it should happen in the Copy Site form, preventing users from submitting an operation we know will fail.

The use-add-site hook already has similar checks that validate directory when the path is selected. Extend this pattern to the Copy Site flow to provide immediate feedback.

Keep the backend validation as a safety net.

sendProgress( 'preparing', __( 'Preparing to copy site...' ), 0 );

const wasSourceRunning = sourceSite.details.running;
if ( wasSourceRunning ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tempServer and newServer are both created with identical parameters (details and targetWpVersion). I think we can create the server once at the beginning and reuse it

await fsPromises.mkdir( nodePath.join( wpContentDest, 'themes' ), { recursive: true } );
}

if ( copyOptions.uploads ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce duplication let's extract this to a helper function for example copyWpContentDirectory.
We could then do something like:

const directories = [
      { name: 'plugins', enabled: copyOptions.plugins, step: 'copying-plugins', percentage: 30 },
      { name: 'themes', enabled: copyOptions.themes, step: 'copying-themes', percentage: 50 },
      { name: 'uploads', enabled: copyOptions.uploads, step: 'copying-uploads', percentage: 60 },
      ...
  ] as const;

  for ( const dir of directories ) {
      await copyWpContentDirectory( ... );
  }

await installSqliteIntegration( newPath );
}

const newServer = SiteServer.create( details, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could do this early on in the process and avoid creating the tempServer on the needsDifferentWpVersion check.


sendProgress( 'finalizing', __( 'Finalizing copy...' ), 90 );

const parentWindow = BrowserWindow.fromWebContents( event.sender );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably only get the parentWindow once at the start and re-use here and in the sendProgress function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add action to duplicate a site

3 participants