PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / AutoLaunch / hooks / useCreateSite.js

useCreateSite.js in Extendify 3.1.4, at src/AutoLaunch/hooks/useCreateSite.js

657 lines 19.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { handleDesignBuild } from '@auto-launch/fetchers/get-design-build';
2 import { handleHome } from '@auto-launch/fetchers/get-home';
3 import { handleSiteImages } from '@auto-launch/fetchers/get-images';
4 import { handleLaunchDecisions } from '@auto-launch/fetchers/get-launch-decisions';
5 import { handleSiteLogo } from '@auto-launch/fetchers/get-logo';
6 import { handlePages } from '@auto-launch/fetchers/get-pages';
7 import { handleSitePlugins } from '@auto-launch/fetchers/get-plugins';
8 import { handleSiteProfile } from '@auto-launch/fetchers/get-profile';
9 import { handleSiteStrings } from '@auto-launch/fetchers/get-strings';
10 import { handleSiteStyle } from '@auto-launch/fetchers/get-style';
11 import {
12 installFontFamilies,
13 mergeFontsIntoVariation,
14 } from '@auto-launch/functions/fonts';
15 import { apiFetchWithTimeout, setStatus } from '@auto-launch/functions/helpers';
16 import { checkIn } from '@auto-launch/functions/insights';
17 import {
18 updateButtonLinks,
19 updateSinglePageLinksToSections,
20 } from '@auto-launch/functions/links';
21 import {
22 addPageLinksToNav,
23 addSectionLinksFromDesign,
24 addSectionLinksToNav,
25 createNavigation,
26 injectNavExtras,
27 injectWooCommerceIcons,
28 updateNavAttributes,
29 } from '@auto-launch/functions/nav';
30 import {
31 addImprintPage,
32 createWpPages,
33 getPagesToCreate,
34 isBlogPage,
35 PLUGIN_OWNED_PAGES,
36 setHelloWorldFeaturedImage,
37 updatePageTitlePattern,
38 } from '@auto-launch/functions/pages';
39 import { generatePageContent } from '@auto-launch/functions/patterns';
40 import {
41 alreadyActive,
42 ensurePluginsActive,
43 getActivePlugins,
44 replacePlaceholderPatterns,
45 reportInactivePlugins,
46 } from '@auto-launch/functions/plugins';
47 import {
48 postLaunchFunctions,
49 prefetchAssistData,
50 } from '@auto-launch/functions/setup';
51 import { applySocialProfiles } from '@auto-launch/functions/social-links';
52 import {
53 setThemeRenderingMode,
54 updateTemplatePart,
55 updateVariation,
56 } from '@auto-launch/functions/theme';
57 import { computeVibeAdjustedBlocks } from '@auto-launch/functions/vibes';
58 import {
59 createBlogSampleData,
60 getOption,
61 getPageById,
62 storeSiteImages,
63 updateOption,
64 } from '@auto-launch/functions/wp';
65 import { useWarnOnLeave } from '@auto-launch/hooks/useWarnOnLeave';
66 import { useLaunchDataStore } from '@auto-launch/state/launch-data';
67 import { digest } from '@shared/api/digest';
68 import { useAIConsentStore } from '@shared/state/ai-consent';
69 import { useEffect, useRef, useState } from '@wordpress/element';
70 import { __ } from '@wordpress/i18n';
71 import useSWRImmutable from 'swr/immutable';
72
73 const { homeUrl, showImprint, wpLanguage } = window.extSharedData;
74
75 // TODO: I think a good strategy is "if something fails, try to refetch some state"
76
77 export const useCreateSite = () => {
78 const { setErrorMessage, addStatusMessage, needToStall, setData, ...data } =
79 useLaunchDataStore();
80 const { setUserGaveConsent } = useAIConsentStore();
81 const homeStretch = useRef(false);
82 const [warnOnReload, setWarnOnReload] = useState(!needToStall());
83 const [done, setDone] = useState(false);
84
85 // We keep the data on reload but show this to prevent movement
86 useWarnOnLeave(warnOnReload, () => {
87 checkIn({ stage: 'exit_early' });
88 });
89
90 // needs: urlParams['build-id']
91 // provides: designBuild, siteProfile, siteStyle
92 useRunStep(
93 'designBuild',
94 () => {
95 if (!data.urlParams?.['build-id']) return null;
96 return data;
97 },
98 handleDesignBuild,
99 );
100
101 // needs: title, descriptionRaw (or designBuild when build-id)
102 // provides: siteProfile: { type, category, description, title, keywords, logoObjectName }
103 useRunStep(
104 'siteProfile',
105 () => {
106 if (!data.descriptionRaw && !data.title) return null;
107 return data;
108 },
109 async (params) => {
110 checkIn({ stage: 'get_profile' });
111 return await handleSiteProfile(params);
112 },
113 );
114
115 // needs: siteProfile
116 // provides: logoUrl
117 useRunStep(
118 'siteLogo',
119 () => {
120 if (!data.siteProfile?.title) return null;
121 // Logo was already uploaded by handleDesignBuild;
122 // generating an AI logo here is not necessary.
123 if (data.designBuild?.logoUrl) return null;
124 return data;
125 },
126 async (params) => {
127 checkIn({ stage: 'get_logo' });
128 return await handleSiteLogo(params);
129 },
130 );
131
132 // needs: siteProfile
133 // provides: sitePlugins: [{name, wordpressSlug}]
134 useRunStep(
135 'sitePlugins',
136 () => {
137 if (!data.siteProfile?.title) return null;
138 return data;
139 },
140 async (params) => {
141 checkIn({ stage: 'get_plugins' });
142 return await handleSitePlugins(params);
143 },
144 );
145
146 useEffect(() => {
147 // Start installing the partner plugins asap
148 if (!data.sitePlugins?.length) return;
149
150 setStatus(
151 // translators: this is for a action log UI. Keep it short
152 __('Setting up functionality for your website', 'extendify-local'),
153 );
154 ensurePluginsActive(
155 data.sitePlugins.map(({ wordpressSlug }) => wordpressSlug),
156 );
157 }, [data.sitePlugins]);
158
159 // needs: siteProfile
160 // provides: style: {}
161 useRunStep(
162 'siteStyle',
163 () => {
164 if (!data.siteProfile?.title) return null;
165 return data;
166 },
167 async (params) => {
168 checkIn({ stage: 'get_style' });
169 return await handleSiteStyle(params);
170 },
171 );
172
173 // needs: siteProfile
174 // provides: aiHeaders: [], aiBlogTitles: []
175 useRunStep(
176 'siteStrings',
177 () => {
178 if (!data.siteProfile?.title) return null;
179 return data;
180 },
181 async (params) => {
182 checkIn({ stage: 'get_strings' });
183 return await handleSiteStrings(params);
184 },
185 );
186
187 // needs: siteProfile
188 // provides: siteImages: []
189 useRunStep(
190 'siteImages',
191 () => {
192 if (!data.siteProfile?.title) return null;
193 return data;
194 },
195 async (params) => {
196 checkIn({ stage: 'get_images' });
197 return await handleSiteImages(params);
198 },
199 );
200
201 // needs: siteProfile
202 // provides: launchDecisions: { navExtras, navButtonLabel }
203 useRunStep(
204 'launchDecisions',
205 () => {
206 if (!data.siteProfile?.title) return null;
207 return data;
208 },
209 async (params) => {
210 checkIn({ stage: 'get_launch_decisions' });
211 return await handleLaunchDecisions(params);
212 },
213 );
214
215 // needs: siteProfile, sitePlugins, siteStyle, siteImages, aiHeaders
216 // provides: home: { id, slug, patterns, siteStyle }
217 useRunStep(
218 'home',
219 () => {
220 const ok = [
221 data.siteProfile,
222 data.siteStyle,
223 data.siteImages,
224 data.sitePlugins,
225 data.aiHeaders,
226 ].every((v) => v !== undefined);
227 return ok ? data : null;
228 },
229 async (params) => {
230 checkIn({ stage: 'get_home' });
231 return await handleHome(params);
232 },
233 );
234
235 // needs: siteProfile, sitePlugins, siteStyle, siteImages
236 // provides: pages: [{ id, slug, name, patterns, siteStyle }]
237 useRunStep(
238 'pages',
239 () => {
240 const ok = [
241 data.siteProfile,
242 data.siteStyle,
243 data.siteImages,
244 data.sitePlugins,
245 ].every((v) => v !== undefined);
246 return ok ? data : null;
247 },
248 async (params) => {
249 checkIn({ stage: 'get_pages' });
250 return await handlePages(params);
251 },
252 );
253
254 // basic defaults
255 useRunStep(
256 'generalUpdates',
257 () => {
258 const ok = [data.siteProfile, data.home, data.pages].every(
259 (v) => v !== undefined,
260 );
261 return ok ? data : null;
262 },
263 async ({ siteProfile, sitePlugins, siteStyle }) => {
264 checkIn({ stage: 'set_general', siteProfile, sitePlugins, siteStyle });
265
266 const { title } = siteProfile;
267 // translators: this is for a action log UI. Keep it short
268 addStatusMessage(__('Adding admin configurations', 'extendify-local'));
269 await updateOption('permalink_structure', '/%postname%/');
270 setUserGaveConsent(true);
271 if (title) await updateOption('blogname', title);
272 },
273 );
274
275 // basic defaults
276 useRunStep(
277 'pluginConfigurations',
278 () => {
279 if (!data.sitePlugins) return null;
280 return data;
281 },
282 async () => {
283 checkIn({ stage: 'set_plugin_config' });
284 const activePlugins = await getActivePlugins();
285 if (alreadyActive(activePlugins, 'wpforms-lite')) {
286 await updateOption('wpforms_activation_redirect', 'skip');
287 }
288 if (alreadyActive(activePlugins, 'all-in-one-seo-pack')) {
289 await updateOption('aioseo_activation_redirect', 'skip');
290 }
291 if (alreadyActive(activePlugins, 'google-analytics-for-wordpress')) {
292 const param = '_transient__monsterinsights_activation_redirect';
293 await updateOption(param, null);
294 }
295 },
296 );
297
298 // If we have home and (maybe) pages then we're ready
299 useEffect(() => {
300 if (needToStall()) return;
301 const {
302 home,
303 pages,
304 siteProfile,
305 sitePlugins,
306 siteStyle,
307 aiBlogTitles,
308 siteImages,
309 designBuild,
310 launchDecisions,
311 } = data;
312 // pages could be [] and pass here, that's ok
313 if (!home || !pages) return;
314 if (homeStretch.current) return;
315 homeStretch.current = true;
316 (async () => {
317 const { objective, structure, category } = siteProfile;
318 const builtHome = designBuild?.builtPages?.find((p) => p.slug === 'home');
319 // Must match get-home.js's full-page test; if they drift, the home is
320 // built one way and planted another.
321 const isSinglePageDesign =
322 structure === 'single-page' &&
323 Boolean(builtHome?.fullPage && builtHome.patterns?.length) &&
324 Boolean(designBuild?.pages?.length);
325 const intendedPlugins = (sitePlugins ?? []).map(
326 ({ wordpressSlug }) => wordpressSlug,
327 );
328
329 // Guarantee plugins are active before the pattern imports below rely on them.
330 await ensurePluginsActive(intendedPlugins);
331
332 const needsImprint = Array.isArray(showImprint)
333 ? showImprint.includes(wpLanguage ?? '') && category === 'Business'
334 : false;
335
336 const customFonts =
337 siteStyle?.variation?.settings?.typography?.fontFamilies?.custom;
338 let variation = siteStyle?.variation;
339 if (customFonts?.length) {
340 checkIn({ stage: 'install_fonts' });
341 // translators: this is for a action log UI. Keep it short
342 addStatusMessage(__('Installing fonts locally', 'extendify-local'));
343 const installed = await installFontFamilies(customFonts).catch(
344 () => [],
345 );
346 variation = mergeFontsIntoVariation(siteStyle.variation, installed);
347 }
348
349 if (siteStyle?.vibe && siteStyle.vibe !== 'natural-1') {
350 // translators: vibe in this context is a noun - the feeling of their site design.
351 addStatusMessage(__('Setting the website style', 'extendify-local'));
352 checkIn({ stage: 'compute_vibe' });
353 const vibeBlocks = await computeVibeAdjustedBlocks(
354 siteStyle.vibe,
355 ).catch(() => null);
356 if (vibeBlocks) {
357 variation = {
358 ...variation,
359 styles: { ...variation.styles, blocks: vibeBlocks },
360 };
361 }
362 }
363
364 checkIn({ stage: 'set_vibe' });
365 await updateVariation(variation);
366
367 // navigation menu
368 addStatusMessage(__('Working on the navigation', 'extendify-local'));
369 const { id: headerNavId } = await createNavigation({
370 title: __('Header Navigation', 'extendify-local'),
371 slug: 'site-navigation',
372 });
373 let headerCode = updateNavAttributes(home.headerCode || '', {
374 ref: headerNavId,
375 });
376 headerCode = injectNavExtras(headerCode, launchDecisions);
377 // remove the header navigation from the landing page
378 if (objective === 'landing-page') {
379 // translators: this is for a action log UI. Keep it short
380 addStatusMessage(__('Perfecting a landing page', 'extendify-local'));
381 const social =
382 /<!--\s*wp:social-links\b[^>]*>.*?<!--\s*\/wp:social-links\s*-->/gis;
383 headerCode = headerCode
384 .replace(/<!--\s*wp:navigation\b[^>]*.*\/-->/gis, '')
385 .replace(social, '');
386 }
387 headerCode = headerCode.replaceAll(
388 '206-555-0100',
389 (typeof siteProfile.phoneNumber === 'string' &&
390 siteProfile.phoneNumber) ||
391 // translators: Use a number that is appropriate for the locale. It does not need to be this exact number. This is a placeholder phone number. For example, in pt_BR you could use (11) 91234-5678.
392 __('206-555-0100', 'extendify-local'),
393 );
394 checkIn({ stage: 'set_navigation' });
395
396 // footer
397 let footerNavId = null;
398 let footerCode = home.footerCode || '';
399 // The logo already carries the brand, so the site title would be redundant.
400 if (designBuild?.hasExternalLogo && footerCode.includes('wp:site-logo')) {
401 footerCode = footerCode.replace(
402 /\s*<!--\s*wp:site-title[\s\S]*?\/-->/g,
403 '',
404 );
405 }
406 if (needsImprint) {
407 const nav = await createNavigation({
408 title: __('Footer Navigation', 'extendify-local'),
409 slug: 'footer-navigation',
410 });
411 footerNavId = nav.id;
412 footerCode = updateNavAttributes(footerCode, { ref: footerNavId });
413 }
414 footerCode = applySocialProfiles(footerCode, siteProfile.socialProfiles);
415 checkIn({ stage: 'set_footer' });
416 await updateTemplatePart('extendable/footer', footerCode);
417
418 // pages
419 // translators: this is for a action log UI. Keep it short
420 addStatusMessage(__('Creating pages', 'extendify-local'));
421 const pagesToCreate = getPagesToCreate(data);
422 const titlePattern = pages?.[0]?.patterns?.find((p) =>
423 p.patternTypes?.includes('page-title'),
424 );
425 if (titlePattern) {
426 checkIn({ stage: 'set_page_title_pattern' });
427 await updatePageTitlePattern(titlePattern.code);
428 }
429
430 const activePlugins = await getActivePlugins();
431 // Keep plugin pages in the nav but skip creating the page itself.
432 const reservedSlugs = new Set(
433 PLUGIN_OWNED_PAGES.filter(
434 ({ plugin }) =>
435 sitePlugins.some((p) => p.wordpressSlug === plugin) ||
436 alreadyActive(activePlugins, plugin),
437 ).map(({ slug }) => slug),
438 );
439 const pagesToActuallyCreate = pagesToCreate.filter(
440 (p) => !reservedSlugs.has(p.slug),
441 );
442
443 // Some patterns have preview html, we can replace those
444 // which may install some plugins too.
445 const pagesReplaced = [];
446 // Run these one page at a time so we don't end up
447 // with duplicate dependency issues
448 checkIn({ stage: 'replace_placeholder_patterns' });
449 for (const page of pagesToActuallyCreate) {
450 const patterns = await replacePlaceholderPatterns(page.patterns);
451 const updatedPage = { ...page, patterns };
452 pagesReplaced.push(updatedPage);
453 }
454 checkIn({ stage: 'generate_page_content' });
455 const customPages = await generatePageContent(pagesReplaced, data);
456
457 // Update heroDescription to the actual AI-rewritten hero content
458 const homePage = customPages.find((p) => p.slug === 'home');
459 const heroPattern = homePage?.patterns?.find((p) =>
460 p.patternTypes?.includes('hero-header'),
461 );
462 const pMatch = heroPattern?.code?.match(/<p[^>]*>([\s\S]*?)<\/p>/);
463 const heroDesc = pMatch?.[1]?.replace(/<[^>]+>/g, '').trim();
464 setData('heroDescription', heroDesc || data.heroDescription);
465
466 const createdPagesWP = await createWpPages(customPages, {
467 skipSectionIds: isSinglePageDesign,
468 });
469 // Aux pages
470 const blogPattern = home?.patterns?.find((pattern) =>
471 pattern.patternTypes.includes('blog-section'),
472 );
473 if (siteProfile.blog || blogPattern) {
474 checkIn({ stage: 'create_blog_sample_data' });
475 // translators: this is for a action log UI. Keep it short
476 addStatusMessage(__('Creating blog sample data', 'extendify-local'));
477 await createBlogSampleData(
478 { aiBlogTitles },
479 siteImages,
480 blogPattern?.blogImages,
481 );
482 }
483 if (siteImages?.length) {
484 checkIn({ stage: 'set_hello_world_image' });
485 await setHelloWorldFeaturedImage(siteImages);
486 }
487
488 let imprint = {};
489 if (needsImprint) {
490 checkIn({ stage: 'create_imprint' });
491 imprint = await addImprintPage({ siteStyle }).catch(() => null);
492 }
493
494 const pluginPages = [];
495 if (alreadyActive(activePlugins, 'woocommerce')) {
496 checkIn({ stage: 'import_woocommerce_products' });
497 addStatusMessage(
498 // translators: this is for a action log UI. Keep it short
499 __('Setting up your online store', 'extendify-local'),
500 );
501 await apiFetchWithTimeout({
502 path: '/extendify/v1/auto-launch/import-woocommerce',
503 }).catch(() => null);
504 const id = await getOption('woocommerce_shop_page_id');
505 const shopPage = id ? await getPageById(id) : null;
506 if (shopPage) pluginPages.push(shopPage);
507 }
508 if (alreadyActive(activePlugins, 'the-events-calendar')) {
509 pluginPages.push({
510 title: { rendered: __('Events', 'extendify-local') },
511 slug: 'events',
512 link: `${homeUrl}/events`,
513 });
514 }
515
516 // The design's page list omits the posts page like it omits shop, so the
517 // nav needs it here — but it's ours to create, not a PLUGIN_OWNED_PAGES.
518 pluginPages.push(...createdPagesWP.filter(isBlogPage));
519
520 // Adding pages to the nav
521 checkIn({ stage: 'set_page_links' });
522 let linksResult = { wpPages: createdPagesWP, headerCode };
523 if (!isSinglePageDesign) {
524 linksResult =
525 structure === 'single-page'
526 ? await updateSinglePageLinksToSections(
527 createdPagesWP,
528 customPages,
529 {
530 objective,
531 activePlugins,
532 landingPageCTALink: siteProfile.landingPageCTALink,
533 },
534 headerCode,
535 )
536 : await updateButtonLinks(createdPagesWP, pluginPages, headerCode);
537 }
538 const pagesWithLinksUpdated = linksResult.wpPages;
539 headerCode = linksResult.headerCode;
540 if (alreadyActive(activePlugins, 'woocommerce')) {
541 headerCode = injectWooCommerceIcons(headerCode);
542 }
543 await updateTemplatePart('extendable/header', headerCode);
544 const footerNavPages = [];
545 if (footerNavId && imprint?.title) {
546 const { originalSlug, title } = imprint;
547 footerNavPages.push({
548 id: originalSlug,
549 name: title.rendered,
550 slug: originalSlug,
551 patterns: [],
552 });
553 }
554
555 checkIn({ stage: 'set_navigation_links' });
556 if (objective !== 'landing-page') {
557 const orderedSlugs = designBuild?.pages?.map((p) => p.slug) ?? [];
558 if (isSinglePageDesign) {
559 await addSectionLinksFromDesign(
560 headerNavId,
561 designBuild.pages,
562 pluginPages,
563 );
564 } else if (structure === 'single-page') {
565 await addSectionLinksToNav(
566 headerNavId,
567 home?.patterns,
568 pluginPages,
569 createdPagesWP,
570 { orderedSlugs },
571 );
572 } else {
573 await addPageLinksToNav(
574 headerNavId,
575 pagesToCreate,
576 pagesWithLinksUpdated,
577 pluginPages,
578 { orderedSlugs },
579 );
580 }
581 if (footerNavId) {
582 await addPageLinksToNav(
583 footerNavId,
584 footerNavPages,
585 imprint?.id
586 ? [...pagesWithLinksUpdated, imprint]
587 : pagesWithLinksUpdated,
588 [],
589 );
590 }
591 }
592
593 checkIn({ stage: 'prefetch_assist_data' });
594 await prefetchAssistData();
595 checkIn({ stage: 'final_steps' });
596 await setThemeRenderingMode('template-locked');
597 await postLaunchFunctions();
598 if (siteImages?.length) {
599 await storeSiteImages(siteImages).catch(() => null);
600 }
601 // translators: this is for a action log UI. Keep it short
602 addStatusMessage(__('All done!', 'extendify-local'));
603 await Promise.all([
604 reportInactivePlugins(intendedPlugins).catch(() => null),
605 checkIn({ stage: 'finished', siteProfile, sitePlugins, siteStyle }),
606 ]);
607 setWarnOnReload(false);
608 setDone(true);
609 })().catch((error) => {
610 console.error(error);
611 digest({
612 error,
613 details: { source: 'auto-launch', caller: 'create-site' },
614 });
615 // if we error here we can try again by resetting the home stretch and stalling again to refetch data
616 homeStretch.current = false;
617 needToStall(true);
618 setErrorMessage(
619 __(
620 'Something went wrong during the final steps. We will try again but you may need to refresh the page.',
621 'extendify-local',
622 ),
623 );
624 });
625 }, [data, needToStall, setUserGaveConsent]);
626
627 return { done };
628 };
629
630 const useRunStep = (stepKey, getParams, fetcher) => {
631 const { setData, setErrorMessage, needToStall } = useLaunchDataStore();
632 const p = getParams?.() ?? null;
633 const { data, error } = useSWRImmutable(
634 p && !needToStall() ? stepKey : null,
635 () => fetcher(getParams()),
636 );
637
638 useEffect(() => {
639 if (!data) return;
640 Object.entries(data).forEach(([k, v]) => {
641 setData(k, v);
642 });
643 }, [data, setData]);
644
645 useEffect(() => {
646 if (!error || needToStall()) return;
647 console.error(error);
648 digest({ error, details: { source: 'auto-launch', caller: 'run-step' } });
649 setErrorMessage(
650 __(
651 'Having some trouble with this step. Trying again...',
652 'extendify-local',
653 ),
654 );
655 }, [error, setErrorMessage, needToStall]);
656 };
657