diff --git a/apps/ui/src/ui-classic/router/layout-onboarding/index.tsx b/apps/ui/src/ui-classic/router/layout-onboarding/index.tsx
index 0db88f49b7..1fb5acd486 100644
--- a/apps/ui/src/ui-classic/router/layout-onboarding/index.tsx
+++ b/apps/ui/src/ui-classic/router/layout-onboarding/index.tsx
@@ -3,7 +3,7 @@ import { OnboardingLayout } from '@/components/onboarding-layout';
import { useSites } from '@/data/queries/use-sites';
import { rootRoute } from '../layout-root';
-function OnboardingShell() {
+export function OnboardingShell() {
const navigate = useNavigate();
const { data: sites } = useSites();
const hasSites = ( sites?.length ?? 0 ) > 0;
diff --git a/apps/ui/src/ui-classic/router/route-onboarding-blueprint/index.tsx b/apps/ui/src/ui-classic/router/route-onboarding-blueprint/index.tsx
index 099c5b8c0c..4793ce7e2a 100644
--- a/apps/ui/src/ui-classic/router/route-onboarding-blueprint/index.tsx
+++ b/apps/ui/src/ui-classic/router/route-onboarding-blueprint/index.tsx
@@ -2,7 +2,7 @@ import {
extractFormValuesFromBlueprint,
updateBlueprintWithFormValues,
} from '@studio/common/lib/blueprint-settings';
-import { createRoute, useNavigate } from '@tanstack/react-router';
+import { createRoute, useNavigate, useSearch } from '@tanstack/react-router';
import { __ } from '@wordpress/i18n';
import { arrowLeft } from '@wordpress/icons';
import { Button, Icon } from '@wordpress/ui';
@@ -24,8 +24,8 @@ interface BlueprintSearch {
step?: Step;
}
-function OnboardingBlueprintPage() {
- const { step } = onboardingBlueprintRoute.useSearch();
+export function OnboardingBlueprintPage() {
+ const { step } = useSearch( { strict: false } ) as BlueprintSearch;
const navigate = useNavigate();
const activeStep: Step = step === 'configure' ? 'configure' : 'select';
@@ -193,15 +193,17 @@ function mapBlueprintSettingsToFormValues(
};
}
+export function validateBlueprintSearch( search: Record< string, unknown > ): BlueprintSearch {
+ const value = search.step;
+ if ( value === 'configure' || value === 'select' ) {
+ return { step: value };
+ }
+ return {};
+}
+
export const onboardingBlueprintRoute = createRoute( {
getParentRoute: () => onboardingLayoutRoute,
path: '/onboarding/blueprint',
- validateSearch: ( search: Record< string, unknown > ): BlueprintSearch => {
- const value = search.step;
- if ( value === 'configure' || value === 'select' ) {
- return { step: value };
- }
- return {};
- },
+ validateSearch: validateBlueprintSearch,
component: OnboardingBlueprintPage,
} );
diff --git a/apps/ui/src/ui-classic/router/route-onboarding-create/index.tsx b/apps/ui/src/ui-classic/router/route-onboarding-create/index.tsx
index bbb98abf50..8ab501c498 100644
--- a/apps/ui/src/ui-classic/router/route-onboarding-create/index.tsx
+++ b/apps/ui/src/ui-classic/router/route-onboarding-create/index.tsx
@@ -11,7 +11,7 @@ import { onboardingLayoutRoute } from '../layout-onboarding';
import styles from '../layout-onboarding/style.module.css';
import type { CreateSiteFormValues } from '@/components/create-site-form';
-function CreateSitePage() {
+export function CreateSitePage() {
const navigate = useNavigate();
const { data: sites } = useSites();
const { data: existingDomainNames } = useExistingCustomDomains();
diff --git a/apps/ui/src/ui-classic/router/route-onboarding-home/index.tsx b/apps/ui/src/ui-classic/router/route-onboarding-home/index.tsx
index be5ebbe680..5c5aef12e2 100644
--- a/apps/ui/src/ui-classic/router/route-onboarding-home/index.tsx
+++ b/apps/ui/src/ui-classic/router/route-onboarding-home/index.tsx
@@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import { onboardingLayoutRoute } from '../layout-onboarding';
import styles from '../layout-onboarding/style.module.css';
-function OnboardingHomePage() {
+export function OnboardingHomePage() {
return (
{ __( 'Start a new site' ) }
diff --git a/apps/ui/src/ui-classic/router/route-onboarding-import/index.tsx b/apps/ui/src/ui-classic/router/route-onboarding-import/index.tsx
index d210ce266b..21b8f6c112 100644
--- a/apps/ui/src/ui-classic/router/route-onboarding-import/index.tsx
+++ b/apps/ui/src/ui-classic/router/route-onboarding-import/index.tsx
@@ -1,5 +1,5 @@
import { ACCEPTED_IMPORT_FILE_TYPES } from '@studio/common/constants';
-import { createRoute, useNavigate } from '@tanstack/react-router';
+import { createRoute, useNavigate, useSearch } from '@tanstack/react-router';
import { __ } from '@wordpress/i18n';
import { arrowLeft, download } from '@wordpress/icons';
import { Button, Icon } from '@wordpress/ui';
@@ -50,8 +50,8 @@ function nameFromFilename( filename: string ): string {
.trim();
}
-function OnboardingImportPage() {
- const { step } = onboardingImportRoute.useSearch();
+export function OnboardingImportPage() {
+ const { step } = useSearch( { strict: false } ) as ImportSearch;
const navigate = useNavigate();
const connector = useConnector();
const activeStep: Step = step === 'configure' ? 'configure' : 'select';
@@ -211,15 +211,17 @@ function OnboardingImportPage() {
);
}
+export function validateImportSearch( search: Record< string, unknown > ): ImportSearch {
+ const value = search.step;
+ if ( value === 'configure' || value === 'select' ) {
+ return { step: value };
+ }
+ return {};
+}
+
export const onboardingImportRoute = createRoute( {
getParentRoute: () => onboardingLayoutRoute,
path: '/onboarding/import',
- validateSearch: ( search: Record< string, unknown > ): ImportSearch => {
- const value = search.step;
- if ( value === 'configure' || value === 'select' ) {
- return { step: value };
- }
- return {};
- },
+ validateSearch: validateImportSearch,
component: OnboardingImportPage,
} );
diff --git a/apps/ui/src/ui-desks/app.tsx b/apps/ui/src/ui-desks/app.tsx
index f8403f2c0a..1371301708 100644
--- a/apps/ui/src/ui-desks/app.tsx
+++ b/apps/ui/src/ui-desks/app.tsx
@@ -2,10 +2,13 @@ import { RouterProvider, createRouter } from '@tanstack/react-router';
import { useMemo } from 'react';
import { createPackagedRouterHistory } from '@/app/router-history';
import {
+ desksDashboardRedirectRoute,
+ desksNewSiteRedirectRoute,
desksOnboardingBlueprintRoute,
desksOnboardingCreateRoute,
desksOnboardingHomeRoute,
desksOnboardingImportRoute,
+ desksOnboardingLayoutRoute,
} from './onboarding';
import { desksRootRoute } from './router/root';
import { siteDeskRoute } from './site-desk';
@@ -16,10 +19,14 @@ const routeTree = desksRootRoute.addChildren( [
userDeskRoute,
desksSiteSettingsRoute,
siteDeskRoute,
- desksOnboardingHomeRoute,
- desksOnboardingCreateRoute,
- desksOnboardingBlueprintRoute,
- desksOnboardingImportRoute,
+ desksOnboardingLayoutRoute.addChildren( [
+ desksOnboardingHomeRoute,
+ desksOnboardingCreateRoute,
+ desksOnboardingBlueprintRoute,
+ desksOnboardingImportRoute,
+ ] ),
+ desksDashboardRedirectRoute,
+ desksNewSiteRedirectRoute,
] );
export function createDesksRouter() {
diff --git a/apps/ui/src/ui-desks/onboarding/index.tsx b/apps/ui/src/ui-desks/onboarding/index.tsx
index 21bb55e602..2466cd4429 100644
--- a/apps/ui/src/ui-desks/onboarding/index.tsx
+++ b/apps/ui/src/ui-desks/onboarding/index.tsx
@@ -1,491 +1,67 @@
-import { ACCEPTED_IMPORT_FILE_TYPES } from '@studio/common/constants';
+import { createRoute, redirect } from '@tanstack/react-router';
+import { OnboardingShell } from '@/ui-classic/router/layout-onboarding';
import {
- extractFormValuesFromBlueprint,
- updateBlueprintWithFormValues,
-} from '@studio/common/lib/blueprint-settings';
-import { createRoute, useNavigate } from '@tanstack/react-router';
-import { __ } from '@wordpress/i18n';
-import { arrowLeft, download } from '@wordpress/icons';
-import { useCallback, useEffect, useState } from 'react';
-import { flushSync } from 'react-dom';
-import { BlueprintSelector, type PickedBlueprint } from '@/components/blueprint-selector';
-import { CreateSiteForm } from '@/components/create-site-form';
-import { FileDropzone } from '@/components/file-dropzone';
-import { OnboardingLayout } from '@/components/onboarding-layout';
-import { useConnector } from '@/data/core';
+ OnboardingBlueprintPage,
+ validateBlueprintSearch,
+} from '@/ui-classic/router/route-onboarding-blueprint';
+import { CreateSitePage } from '@/ui-classic/router/route-onboarding-create';
+import { OnboardingHomePage } from '@/ui-classic/router/route-onboarding-home';
import {
- useExistingCustomDomains,
- useProposedSiteName,
-} from '@/data/queries/use-create-site-helpers';
-import { useFeaturedBlueprints } from '@/data/queries/use-featured-blueprints';
-import { useImportSite } from '@/data/queries/use-import-site';
-import { useCreateSite, useSites } from '@/data/queries/use-sites';
-import { Button } from '@/ui-desks/components';
+ OnboardingImportPage,
+ validateImportSearch,
+} from '@/ui-classic/router/route-onboarding-import';
import { desksRootRoute } from '../router/root';
-import styles from './style.module.css';
-import type { CreateSiteFormValues } from '@/components/create-site-form';
-type Step = 'select' | 'configure';
+/**
+ * The desks surface has no site-creation flow of its own. It registers the
+ * classic onboarding pages under its own route tree, then adapts the flow's
+ * exit URLs to their desks equivalents.
+ */
-interface StepSearch {
- step?: Step;
-}
-
-interface PickedBackup {
- file: File;
- path: string;
-}
-
-export function DeskOnboardingHome() {
- const navigate = useNavigate();
-
- return (
-
void navigate( { to: '/' } ) } width="wide">
-
-
{ __( 'Start a new site' ) }
-
- { __( 'WordPress can power anything. What are you building?' ) }
-
-
-
-
-
-
-
-
- );
-}
-
-export function DeskOnboardingCreate() {
- const navigate = useNavigate();
- const { data: sites } = useSites();
- const { data: existingDomainNames } = useExistingCustomDomains();
- const { data: proposedName } = useProposedSiteName( sites );
- const createSite = useCreateSite();
- const [ submitError, setSubmitError ] = useState( '' );
-
- const handleSubmit = async ( values: CreateSiteFormValues ) => {
- setSubmitError( '' );
- try {
- const site = await createSite.mutateAsync( {
- name: values.name,
- path: values.path,
- phpVersion: values.phpVersion,
- wpVersion: values.wpVersion,
- customDomain: values.customDomain,
- enableHttps: values.enableHttps,
- adminUsername: values.adminUsername || undefined,
- adminPassword: values.adminPassword || undefined,
- adminEmail: values.adminEmail || undefined,
- } );
- await navigate( { to: '/sites/$siteId', params: { siteId: site.id } } );
- } catch ( error ) {
- setSubmitError(
- error instanceof Error ? error.message : __( 'Failed to create site. Please try again.' )
- );
- }
- };
-
- return (
-
void navigate( { to: '/' } ) }>
-
-
{ __( 'Create a new site' ) }
-
- { __( 'Choose a name and we\u2019ll scaffold a fresh WordPress site locally.' ) }
-
-
void navigate( { to: '/onboarding' } ) }
- isSubmitting={ createSite.isPending }
- submitError={ submitError }
- />
-
-
- );
-}
-
-export function DeskOnboardingBlueprint() {
- const { step } = desksOnboardingBlueprintRoute.useSearch() as StepSearch;
- const navigate = useNavigate();
- const activeStep: Step = step === 'configure' ? 'configure' : 'select';
- const featured = useFeaturedBlueprints();
- const { data: existingDomainNames } = useExistingCustomDomains();
- const createSite = useCreateSite();
- const [ picked, setPicked ] = useState< PickedBlueprint | null >( null );
- const [ submitError, setSubmitError ] = useState( '' );
-
- useEffect( () => {
- if ( activeStep === 'configure' && ! picked ) {
- void navigate( {
- to: '/onboarding/blueprint',
- search: { step: 'select' },
- replace: true,
- } );
- }
- }, [ activeStep, picked, navigate ] );
-
- const handlePick = useCallback(
- ( blueprint: PickedBlueprint ) => {
- flushSync( () => {
- setPicked( blueprint );
- setSubmitError( '' );
- } );
- void navigate( {
- to: '/onboarding/blueprint',
- search: { step: 'configure' },
- } );
- },
- [ navigate ]
- );
-
- const handleSubmit = async ( values: CreateSiteFormValues ) => {
- if ( ! picked ) return;
- setSubmitError( '' );
- const mergedBlueprint = updateBlueprintWithFormValues( picked.blueprint, {
- phpVersion: values.phpVersion,
- wpVersion: values.wpVersion,
- customDomain: values.customDomain,
- enableHttps: values.enableHttps,
- adminUsername: values.adminUsername,
- adminPassword: values.adminPassword,
- siteName: values.name,
- } );
- try {
- const site = await createSite.mutateAsync( {
- name: values.name,
- path: values.path,
- phpVersion: values.phpVersion,
- wpVersion: values.wpVersion,
- customDomain: values.customDomain,
- enableHttps: values.enableHttps,
- adminUsername: values.adminUsername || undefined,
- adminPassword: values.adminPassword || undefined,
- adminEmail: values.adminEmail || undefined,
- blueprint: {
- blueprint: mergedBlueprint,
- slug: picked.slug,
- filePath: picked.filePath,
- },
- } );
- await navigate( { to: '/sites/$siteId', params: { siteId: site.id } } );
- } catch ( error ) {
- setSubmitError(
- error instanceof Error
- ? error.message
- : __( 'Failed to create site from Blueprint. Please try again.' )
- );
- }
- };
-
- if ( activeStep === 'select' ) {
- return (
-
void navigate( { to: '/' } ) } width="wide">
-
-
{ __( 'Start from a Blueprint' ) }
-
- { __(
- 'Pick a featured Blueprint or drop in your own to provision plugins, content, and settings.'
- ) }
-
-
-
-
- );
- }
-
- if ( ! picked ) {
- return null;
- }
-
- const initialValues = mapBlueprintSettingsToFormValues(
- extractFormValuesFromBlueprint( picked.blueprint ),
- picked.title
- );
-
- return (
-
void navigate( { to: '/' } ) }>
-
-
-
{ picked.title }
- { picked.excerpt &&
{ picked.excerpt }
}
-
void navigate( { to: '/onboarding' } ) }
- isSubmitting={ createSite.isPending }
- submitError={ submitError }
- submitLabel={ __( 'Create site from Blueprint' ) }
- />
-
-
- );
-}
-
-export function DeskOnboardingImport() {
- const { step } = desksOnboardingImportRoute.useSearch() as StepSearch;
- const navigate = useNavigate();
- const connector = useConnector();
- const activeStep: Step = step === 'configure' ? 'configure' : 'select';
- const { data: existingDomainNames } = useExistingCustomDomains();
- const createSite = useCreateSite();
- const importSite = useImportSite();
- const [ picked, setPicked ] = useState< PickedBackup | null >( null );
- const [ pickError, setPickError ] = useState< string | null >( null );
- const [ submitError, setSubmitError ] = useState( '' );
-
- useEffect( () => {
- if ( activeStep === 'configure' && ! picked ) {
- void navigate( {
- to: '/onboarding/import',
- search: { step: 'select' },
- replace: true,
- } );
- }
- }, [ activeStep, picked, navigate ] );
-
- const handlePick = useCallback(
- async ( file: File ) => {
- if ( ! isValidBackupFile( file ) ) {
- setPickError(
- __(
- 'This file type is not supported. Please use a .zip, .gz, .tar, .tar.gz, or .wpress file.'
- )
- );
- return;
- }
- const path = await connector.getFilePath( file );
- if ( ! path ) {
- setPickError(
- __( 'Unable to resolve the backup file path. Try choosing the file via the button.' )
- );
- return;
- }
- flushSync( () => {
- setPickError( null );
- setPicked( { file, path } );
- } );
- void navigate( {
- to: '/onboarding/import',
- search: { step: 'configure' },
- } );
- },
- [ connector, navigate ]
- );
-
- const handleClearPick = useCallback( () => {
- setPicked( null );
- setPickError( null );
- }, [] );
-
- const handleSubmit = async ( values: CreateSiteFormValues ) => {
- if ( ! picked ) return;
- setSubmitError( '' );
- try {
- const site = await createSite.mutateAsync( {
- name: values.name,
- path: values.path,
- phpVersion: values.phpVersion,
- wpVersion: values.wpVersion,
- customDomain: values.customDomain,
- enableHttps: values.enableHttps,
- adminUsername: values.adminUsername || undefined,
- adminPassword: values.adminPassword || undefined,
- adminEmail: values.adminEmail || undefined,
- } );
- await importSite.mutateAsync( {
- siteId: site.id,
- backup: { path: picked.path, type: picked.file.type },
- } );
- await navigate( { to: '/sites/$siteId', params: { siteId: site.id } } );
- } catch ( error ) {
- setSubmitError(
- error instanceof Error ? error.message : __( 'Failed to import site. Please try again.' )
- );
- }
- };
-
- if ( activeStep === 'select' ) {
- return (
-
void navigate( { to: '/' } ) }>
-
-
{ __( 'Import from a backup' ) }
-
- { __(
- 'Drop a backup archive to restore a site locally. Jetpack, All-in-One WP Migration, Local, and Playground exports are supported.'
- ) }
-
-
void handlePick( file ) }
- file={ picked?.file ?? null }
- onClear={ handleClearPick }
- error={ pickError }
- />
-
-
- );
- }
-
- if ( ! picked ) {
- return null;
- }
-
- const initialValues: Partial< CreateSiteFormValues > = {
- name: nameFromFilename( picked.file.name ),
- };
- const isSubmitting = createSite.isPending || importSite.isPending;
-
- return (
-
void navigate( { to: '/' } ) }>
-
-
-
{ __( 'Configure the imported site' ) }
-
- { __( 'Pick a name and local folder. The backup will restore on top of this new site.' ) }
-
-
void navigate( { to: '/onboarding' } ) }
- isSubmitting={ isSubmitting }
- submitError={ submitError }
- submitLabel={ __( 'Import site' ) }
- />
-
-
- );
-}
-
-function mapBlueprintSettingsToFormValues(
- settings: ReturnType< typeof extractFormValuesFromBlueprint >,
- fallbackName: string
-): Partial< CreateSiteFormValues > {
- return {
- name: settings.siteName || fallbackName,
- phpVersion: settings.phpVersion,
- wpVersion: settings.wpVersion,
- customDomain: settings.customDomain,
- enableHttps: settings.enableHttps,
- adminUsername: settings.adminUsername,
- adminPassword: settings.adminPassword,
- };
-}
-
-function isValidBackupFile( file: File ): boolean {
- const lower = file.name.toLowerCase();
- return ACCEPTED_IMPORT_FILE_TYPES.some( ( ext ) => lower.endsWith( ext ) );
-}
-
-function nameFromFilename( filename: string ): string {
- const basename = filename.replace( /^.*[\\/]/, '' );
- const lower = basename.toLowerCase();
- const ext = ACCEPTED_IMPORT_FILE_TYPES.find( ( candidate ) => lower.endsWith( candidate ) );
- return ( ext ? basename.slice( 0, -ext.length ) : basename )
- .replace( /[-_](backup|export|wordpress|jetpack)(s)?$/i, '' )
- .replace( /[-_]\d{4}[-_]\d{2}[-_]\d{2}.*$/, '' )
- .replace( /[-_]+/g, ' ' )
- .trim();
-}
-
-function validateStepSearch( search: Record< string, unknown > ): StepSearch {
- const value = search.step;
- if ( value === 'configure' || value === 'select' ) {
- return { step: value };
- }
- return {};
-}
+export const desksOnboardingLayoutRoute = createRoute( {
+ getParentRoute: () => desksRootRoute,
+ id: 'desks-onboarding-layout',
+ component: OnboardingShell,
+} );
export const desksOnboardingHomeRoute = createRoute( {
- getParentRoute: () => desksRootRoute,
+ getParentRoute: () => desksOnboardingLayoutRoute,
path: '/onboarding',
- component: DeskOnboardingHome,
+ component: OnboardingHomePage,
} );
export const desksOnboardingCreateRoute = createRoute( {
- getParentRoute: () => desksRootRoute,
+ getParentRoute: () => desksOnboardingLayoutRoute,
path: '/onboarding/create',
- component: DeskOnboardingCreate,
+ component: CreateSitePage,
} );
export const desksOnboardingBlueprintRoute = createRoute( {
- getParentRoute: () => desksRootRoute,
+ getParentRoute: () => desksOnboardingLayoutRoute,
path: '/onboarding/blueprint',
- validateSearch: validateStepSearch,
- component: DeskOnboardingBlueprint,
+ validateSearch: validateBlueprintSearch,
+ component: OnboardingBlueprintPage,
} );
export const desksOnboardingImportRoute = createRoute( {
- getParentRoute: () => desksRootRoute,
+ getParentRoute: () => desksOnboardingLayoutRoute,
path: '/onboarding/import',
- validateSearch: validateStepSearch,
- component: DeskOnboardingImport,
+ validateSearch: validateImportSearch,
+ component: OnboardingImportPage,
+} );
+
+export const desksDashboardRedirectRoute = createRoute( {
+ getParentRoute: () => desksRootRoute,
+ path: '/dashboard',
+ beforeLoad: () => {
+ throw redirect( { to: '/' } );
+ },
+} );
+
+export const desksNewSiteRedirectRoute = createRoute( {
+ getParentRoute: () => desksRootRoute,
+ path: '/sites/$siteId/new',
+ beforeLoad: ( { params } ) => {
+ throw redirect( { to: '/sites/$siteId', params } );
+ },
} );
diff --git a/apps/ui/src/ui-desks/onboarding/style.module.css b/apps/ui/src/ui-desks/onboarding/style.module.css
deleted file mode 100644
index f6c171e58b..0000000000
--- a/apps/ui/src/ui-desks/onboarding/style.module.css
+++ /dev/null
@@ -1,56 +0,0 @@
-.page {
- width: 100%;
- text-align: left;
-}
-
-.title {
- margin-bottom: 8px;
-}
-
-.subtitle {
- color: var(--wpds-color-fg-content-neutral-weak, #666);
- margin: 0 0 32px;
-}
-
-.cards {
- display: flex;
- gap: 16px;
-}
-
-.card {
- position: relative;
- display: block;
- text-align: left;
- padding: 24px;
- width: 240px;
- border: 1px solid var(--wpds-color-stroke-surface-neutral, #ddd);
- border-radius: 8px;
- cursor: pointer;
- color: inherit;
- text-decoration: none;
- background: var(--wpds-color-bg-surface-neutral-strong, #fff);
- font: inherit;
-}
-
-.card:hover {
- border-color: var(--wpds-color-stroke-interactive-neutral, #bbb);
-}
-
-.cardTitle {
- font-weight: 600;
- margin-bottom: 8px;
-}
-
-.cardBody {
- color: var(--wpds-color-fg-content-neutral-weak, #666);
- font-size: 0.875rem;
-}
-
-.backLink {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- align-self: flex-start;
- margin-bottom: 16px;
- padding: 4px 8px 4px 4px;
-}