Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cf47bdb
Remove enableMultisite from unsupported blueprint features
bcotrim Jan 23, 2026
fc214a2
Merge branch 'trunk' into stu-831-add-blueprints-multisite-support
bcotrim Jan 30, 2026
7092133
Merge remote-tracking branch 'origin/trunk' into stu-831-add-blueprin…
bcotrim Feb 5, 2026
1fe05d5
Merge remote-tracking branch 'origin/trunk' into stu-831-add-blueprin…
bcotrim Feb 18, 2026
531b085
Bump @wp-playground packages to 3.0.54
bcotrim Feb 18, 2026
bf69222
Add multisite validation requiring custom domain for enableMultisite …
bcotrim Feb 18, 2026
9195747
Fix lint: import order and formatting
bcotrim Feb 18, 2026
e156a07
Merge remote-tracking branch 'origin/trunk' into stu-831-add-blueprin…
bcotrim Feb 19, 2026
94ff1e8
Merge remote-tracking branch 'origin/trunk' into stu-831-add-blueprin…
bcotrim Feb 19, 2026
d1da9bb
Merge remote-tracking branch 'origin/trunk' into stu-831-add-blueprin…
bcotrim Feb 20, 2026
76f1610
Extract shared applyBlueprintFormValues utility to reduce duplication
bcotrim Feb 25, 2026
823ee53
Inject SQLITE_MAIN_FILE define into db.php dropin
bcotrim Feb 26, 2026
4448ca8
Merge branch 'trunk' into stu-831-add-blueprints-multisite-support
bcotrim Feb 26, 2026
ceff5d5
Use warning notice for multisite custom domain requirement
bcotrim Feb 27, 2026
eb725b2
Merge branch 'trunk' into stu-831-add-blueprints-multisite-support
bcotrim Feb 27, 2026
54338bc
Remove login from unsupported blueprint features after trunk merge
bcotrim Feb 27, 2026
4762b27
Add missing extractFormValuesFromBlueprint import in add-site
bcotrim Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion apps/cli/commands/site/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
DEFAULT_WORDPRESS_VERSION,
MINIMUM_WORDPRESS_VERSION,
} from '@studio/common/constants';
import { extractFormValuesFromBlueprint } from '@studio/common/lib/blueprint-settings';
import {
blueprintHasMultisite,
extractFormValuesFromBlueprint,
} from '@studio/common/lib/blueprint-settings';
import {
filterUnsupportedBlueprintFeatures,
validateBlueprintData,
Expand Down Expand Up @@ -142,6 +145,14 @@ export async function runCommand(
blueprint = filterUnsupportedBlueprintFeatures(
options.blueprint.contents as Record< string, unknown >
);

if ( blueprint && blueprintHasMultisite( blueprint ) && ! options.customDomain ) {
throw new LoggerError(
__(
'The enableMultisite Blueprint step requires a custom domain. WordPress multisite does not support custom ports. Use --domain <name>.local to set a custom domain.'
)
);
}
}

const appdata = await readAppdata();
Expand Down
35 changes: 35 additions & 0 deletions apps/cli/commands/site/tests/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,41 @@ describe( 'CLI: studio site create', () => {
} );
} );

describe( 'Multisite Validation', () => {
it( 'should error when enableMultisite step is present without custom domain', async () => {
const multisiteBlueprint: Blueprint = {
steps: [ { step: 'enableMultisite' } ],
};

await expect(
runCommand( mockSitePath, {
...defaultTestOptions,
blueprint: {
uri: '/home/test/blueprint.json',
contents: multisiteBlueprint,
},
} )
).rejects.toThrow( /enableMultisite.*custom domain/i );
} );

it( 'should proceed when enableMultisite step is present with custom domain', async () => {
const multisiteBlueprint: Blueprint = {
steps: [ { step: 'enableMultisite' } ],
};

await runCommand( mockSitePath, {
...defaultTestOptions,
customDomain: 'test.local',
blueprint: {
uri: '/home/test/blueprint.json',
contents: multisiteBlueprint,
},
} );

expect( startWordPressServer ).toHaveBeenCalled();
} );
} );

describe( 'noStart Option', () => {
it( 'should not start server when noStart is true', async () => {
await runCommand( mockSitePath, {
Expand Down
6 changes: 6 additions & 0 deletions apps/studio/src/hooks/use-add-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function useAddSite() {
const [ blueprintSuggestedSiteName, setBlueprintSuggestedSiteName ] = useState<
string | undefined
>();
const [ blueprintRequiresCustomDomain, setBlueprintRequiresCustomDomain ] = useState( false );
const [ isDeeplinkFlow, setIsDeeplinkFlow ] = useState( false );
const [ existingDomainNames, setExistingDomainNames ] = useState< string[] >( [] );

Expand All @@ -89,6 +90,7 @@ export function useAddSite() {
setBlueprintSuggestedDomain( undefined );
setBlueprintSuggestedHttps( undefined );
setBlueprintSuggestedSiteName( undefined );
setBlueprintRequiresCustomDomain( false );
}, [] );

// For blueprint deeplinks - we need temporary state for PHP/WP versions
Expand All @@ -105,6 +107,7 @@ export function useAddSite() {
setBlueprintSuggestedDomain( undefined );
setBlueprintSuggestedHttps( undefined );
setBlueprintSuggestedSiteName( undefined );
setBlueprintRequiresCustomDomain( false );
setSelectedRemoteSite( undefined );
setDeeplinkPhpVersion( defaultPhpVersion as AllowedPHPVersion );
setDeeplinkWpVersion( defaultWordPressVersion );
Expand Down Expand Up @@ -341,6 +344,8 @@ export function useAddSite() {
setBlueprintSuggestedHttps,
blueprintSuggestedSiteName,
setBlueprintSuggestedSiteName,
blueprintRequiresCustomDomain,
setBlueprintRequiresCustomDomain,
selectedRemoteSite,
setSelectedRemoteSite,
existingDomainNames,
Expand Down Expand Up @@ -368,6 +373,7 @@ export function useAddSite() {
blueprintSuggestedDomain,
blueprintSuggestedHttps,
blueprintSuggestedSiteName,
blueprintRequiresCustomDomain,
selectedRemoteSite,
existingDomainNames,
loadAllCustomDomains,
Expand Down
18 changes: 18 additions & 0 deletions apps/studio/src/modules/add-site/components/create-site-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ interface CreateSiteFormProps {
blueprintSuggestedDomain?: string;
/** Blueprint suggested HTTPS setting from defineSiteUrl step */
blueprintSuggestedHttps?: boolean;
/** Whether the blueprint requires a custom domain (e.g., multisite) */
blueprintRequiresCustomDomain?: boolean;
/** Blueprint login credentials for pre-filling admin fields */
blueprintCredentials?: { adminUsername?: string; adminPassword?: string };
/** Called when form is submitted */
Expand Down Expand Up @@ -168,6 +170,7 @@ export const CreateSiteForm = ( {
blueprintPreferredVersions,
blueprintSuggestedDomain,
blueprintSuggestedHttps,
blueprintRequiresCustomDomain,
blueprintCredentials,
onSubmit,
onValidityChange,
Expand Down Expand Up @@ -259,6 +262,14 @@ export const CreateSiteForm = ( {
setAdvancedSettingsVisible( true );
}, [ blueprintSuggestedDomain, blueprintSuggestedHttps ] );

useEffect( () => {
if ( ! blueprintRequiresCustomDomain ) {
return;
}
setUseCustomDomain( true );
setAdvancedSettingsVisible( true );
}, [ blueprintRequiresCustomDomain ] );

useEffect( () => {
if ( useCustomDomain && isCertificateTrusted ) {
setEnableHttps( true );
Expand Down Expand Up @@ -649,11 +660,18 @@ export const CreateSiteForm = ( {
type="checkbox"
id="use-custom-domain"
checked={ useCustomDomain }
disabled={ blueprintRequiresCustomDomain }
onChange={ ( e ) => setUseCustomDomain( e.target.checked ) }
/>
<label htmlFor="use-custom-domain">{ __( 'Use custom domain' ) }</label>
</div>

{ blueprintRequiresCustomDomain && (
<Notice status="warning" isDismissible={ false } className="mt-2">
{ __( 'WordPress multisite requires a custom domain.' ) }
</Notice>
) }

<div className="text-a8c-gray-50 text-xs mt-2">
{ __( 'Your system password will be required to set up the domain.' ) }
</div>
Expand Down
3 changes: 3 additions & 0 deletions apps/studio/src/modules/add-site/components/create-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface CreateSiteProps {
blueprintPreferredVersions?: BlueprintPreferredVersions;
blueprintSuggestedDomain?: string;
blueprintSuggestedHttps?: boolean;
blueprintRequiresCustomDomain?: boolean;
blueprintCredentials?: { adminUsername?: string; adminPassword?: string };
originalDefaultVersions?: {
phpVersion?: AllowedPHPVersion;
Expand All @@ -40,6 +41,7 @@ export default function CreateSite( {
blueprintPreferredVersions,
blueprintSuggestedDomain,
blueprintSuggestedHttps,
blueprintRequiresCustomDomain,
blueprintCredentials,
onSubmit,
onValidityChange,
Expand All @@ -61,6 +63,7 @@ export default function CreateSite( {
blueprintPreferredVersions={ blueprintPreferredVersions }
blueprintSuggestedDomain={ blueprintSuggestedDomain }
blueprintSuggestedHttps={ blueprintSuggestedHttps }
blueprintRequiresCustomDomain={ blueprintRequiresCustomDomain }
blueprintCredentials={ blueprintCredentials }
onSubmit={ onSubmit }
onValidityChange={ onValidityChange }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe( 'useBlueprintDeeplink', () => {
const mockSetBlueprintSuggestedDomain = vi.fn();
const mockSetBlueprintSuggestedHttps = vi.fn();
const mockSetBlueprintSuggestedSiteName = vi.fn();
const mockSetBlueprintRequiresCustomDomain = vi.fn();
const mockSetIsDeeplinkFlow = vi.fn();
let ipcCallback: Parameters< typeof useIpcListener >[ 1 ];

Expand All @@ -40,6 +41,7 @@ describe( 'useBlueprintDeeplink', () => {
setBlueprintSuggestedDomain: mockSetBlueprintSuggestedDomain,
setBlueprintSuggestedHttps: mockSetBlueprintSuggestedHttps,
setBlueprintSuggestedSiteName: mockSetBlueprintSuggestedSiteName,
setBlueprintRequiresCustomDomain: mockSetBlueprintRequiresCustomDomain,
setIsDeeplinkFlow: mockSetIsDeeplinkFlow,
} ),
{ wrapper }
Expand Down Expand Up @@ -227,8 +229,8 @@ describe( 'useBlueprintDeeplink', () => {
} );
} );

expect( mockSetBlueprintSuggestedDomain ).not.toHaveBeenCalled();
expect( mockSetBlueprintSuggestedHttps ).not.toHaveBeenCalled();
expect( mockSetBlueprintSuggestedDomain ).toHaveBeenCalledWith( undefined );
expect( mockSetBlueprintSuggestedHttps ).toHaveBeenCalledWith( undefined );
} );

it( 'should set site name from setSiteOptions blogname', async () => {
Expand Down
42 changes: 15 additions & 27 deletions apps/studio/src/modules/add-site/hooks/use-blueprint-deeplink.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { generateDefaultBlueprintDescription } from '@studio/common/lib/blueprint-settings';
import {
extractFormValuesFromBlueprint,
generateDefaultBlueprintDescription,
} from '@studio/common/lib/blueprint-settings';
import {
BlueprintValidationWarning,
BlueprintPreferredVersions,
BlueprintValidationWarning,
} from '@studio/common/lib/blueprint-validation';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback } from 'react';
import { useIpcListener } from 'src/hooks/use-ipc-listener';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { Blueprint } from 'src/stores/wpcom-api';
import { applyBlueprintFormValues } from '../lib/apply-blueprint-form-values';

type BlueprintMetadata = {
title?: string;
Expand All @@ -27,12 +24,12 @@ interface UseBlueprintDeeplinkOptions {
setBlueprintSuggestedDomain: ( domain: string | undefined ) => void;
setBlueprintSuggestedHttps: ( https: boolean | undefined ) => void;
setBlueprintSuggestedSiteName: ( name: string | undefined ) => void;
setBlueprintRequiresCustomDomain: ( requires: boolean ) => void;
setIsDeeplinkFlow: ( isDeeplink: boolean ) => void;
onModalOpen?: () => void;
}

export function useBlueprintDeeplink( options: UseBlueprintDeeplinkOptions ): void {
const { __ } = useI18n();
const {
isAnySiteProcessing,
setSelectedBlueprint,
Expand All @@ -43,6 +40,7 @@ export function useBlueprintDeeplink( options: UseBlueprintDeeplinkOptions ): vo
setBlueprintSuggestedDomain,
setBlueprintSuggestedHttps,
setBlueprintSuggestedSiteName,
setBlueprintRequiresCustomDomain,
setIsDeeplinkFlow,
onModalOpen,
} = options;
Expand Down Expand Up @@ -80,26 +78,15 @@ export function useBlueprintDeeplink( options: UseBlueprintDeeplinkOptions ): vo

setSelectedBlueprint( fileBlueprint );

const formValues = extractFormValuesFromBlueprint( blueprintJson );

if ( blueprintJson.preferredVersions ) {
setBlueprintPreferredVersions(
blueprintJson.preferredVersions as BlueprintPreferredVersions
);
}
if ( formValues.phpVersion ) {
setPhpVersion( formValues.phpVersion );
}
if ( formValues.wpVersion ) {
setWpVersion( formValues.wpVersion );
}
if ( formValues.customDomain ) {
setBlueprintSuggestedDomain( formValues.customDomain );
setBlueprintSuggestedHttps( formValues.enableHttps );
}
if ( formValues.siteName ) {
setBlueprintSuggestedSiteName( formValues.siteName );
}
applyBlueprintFormValues( blueprintJson, {
setBlueprintPreferredVersions,
setPhpVersion,
setWpVersion,
setBlueprintSuggestedDomain,
setBlueprintSuggestedHttps,
setBlueprintSuggestedSiteName,
setBlueprintRequiresCustomDomain,
} );

setBlueprintWarnings( warnings );
setIsDeeplinkFlow( true );
Expand All @@ -118,6 +105,7 @@ export function useBlueprintDeeplink( options: UseBlueprintDeeplinkOptions ): vo
setBlueprintSuggestedDomain,
setBlueprintSuggestedHttps,
setBlueprintSuggestedSiteName,
setBlueprintRequiresCustomDomain,
setIsDeeplinkFlow,
onModalOpen,
]
Expand Down
Loading