PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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.2.1, at src/AutoLaunch/hooks/useCreateSite.js

659 lines 19.9 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 reportFailedDependencies,
46 reportInactivePlugins,
47 } from '@auto-launch/functions/plugins';
48 import {
49 postLaunchFunctions,
50 prefetchAssistData,
51 } from '@auto-launch/functions/setup';
52 import { applySocialProfiles } from '@auto-launch/functions/social-links';
53 import {
54 setThemeRenderingMode,
55 updateTemplatePart,
56 updateVariation,
57 } from '@auto-launch/functions/theme';
58 import { computeVibeAdjustments } from '@auto-launch/functions/vibes';
59 import {
60 createBlogSampleData,
61 getOption,
62 getPageById,
63 storeSiteImages,
64 updateOption,
65 } from '@auto-launch/functions/wp';
66 import { useWarnOnLeave } from '@auto-launch/hooks/useWarnOnLeave';
67 import { useLaunchDataStore } from '@auto-launch/state/launch-data';
68 import { launchStrings } from '@auto-launch/strings';
69 import { digest } from '@shared/api/digest';
70 import { siteImageUrls } from '@shared/lib/site-images';
71 import { useAIConsentStore } from '@shared/state/ai-consent';
72 import { useEffect, useRef, useState } from '@wordpress/element';
73 import { __ } from '@wordpress/i18n';
74 import useSWRImmutable from 'swr/immutable';
75
76 const { homeUrl, showImprint, wpLanguage } = window.extSharedData;
77
78 // TODO: I think a good strategy is "if something fails, try to refetch some state"
79
80 export const useCreateSite = () => {
81 const { setErrorMessage, addStatusMessage, needToStall, setData, ...data } =
82 useLaunchDataStore();
83 const { setUserGaveConsent } = useAIConsentStore();
84 const homeStretch = useRef(false);
85 const [warnOnReload, setWarnOnReload] = useState(!needToStall());
86 const [done, setDone] = useState(false);
87
88 // We keep the data on reload but show this to prevent movement
89 useWarnOnLeave(warnOnReload, () => {
90 checkIn({ stage: 'exit_early' });
91 });
92
93 // needs: urlParams['build-id']
94 // provides: designBuild, siteProfile, siteStyle
95 useRunStep(
96 'designBuild',
97 () => {
98 if (!data.urlParams?.['build-id']) return null;
99 return data;
100 },
101 handleDesignBuild,
102 );
103
104 // needs: title, descriptionRaw (or designBuild when build-id)
105 // provides: siteProfile: { type, category, description, title, keywords, logoObjectName }
106 useRunStep(
107 'siteProfile',
108 () => {
109 if (!data.descriptionRaw && !data.title) return null;
110 return data;
111 },
112 async (params) => {
113 checkIn({ stage: 'get_profile' });
114 return await handleSiteProfile(params);
115 },
116 );
117
118 // needs: siteProfile
119 // provides: logoUrl
120 useRunStep(
121 'siteLogo',
122 () => {
123 if (!data.siteProfile?.title) return null;
124 // Logo was already uploaded by handleDesignBuild;
125 // generating an AI logo here is not necessary.
126 if (data.designBuild?.logoUrl) return null;
127 return data;
128 },
129 async (params) => {
130 checkIn({ stage: 'get_logo' });
131 return await handleSiteLogo(params);
132 },
133 );
134
135 // needs: siteProfile
136 // provides: sitePlugins: [{name, wordpressSlug}]
137 useRunStep(
138 'sitePlugins',
139 () => {
140 if (!data.siteProfile?.title) return null;
141 return data;
142 },
143 async (params) => {
144 checkIn({ stage: 'get_plugins' });
145 return await handleSitePlugins(params);
146 },
147 );
148
149 useEffect(() => {
150 // Start installing the partner plugins asap
151 if (!data.sitePlugins?.length) return;
152
153 setStatus(
154 // translators: this is for a action log UI. Keep it short
155 launchStrings().statusFunctionality,
156 );
157 ensurePluginsActive(
158 data.sitePlugins.map(({ wordpressSlug }) => wordpressSlug),
159 );
160 }, [data.sitePlugins]);
161
162 // needs: siteProfile
163 // provides: style: {}
164 useRunStep(
165 'siteStyle',
166 () => {
167 if (!data.siteProfile?.title) return null;
168 return data;
169 },
170 async (params) => {
171 checkIn({ stage: 'get_style' });
172 return await handleSiteStyle(params);
173 },
174 );
175
176 // needs: siteProfile
177 // provides: aiHeaders: [], aiBlogTitles: []
178 useRunStep(
179 'siteStrings',
180 () => {
181 if (!data.siteProfile?.title) return null;
182 return data;
183 },
184 async (params) => {
185 checkIn({ stage: 'get_strings' });
186 return await handleSiteStrings(params);
187 },
188 );
189
190 // needs: siteProfile
191 // provides: siteImages: []
192 useRunStep(
193 'siteImages',
194 () => {
195 if (!data.siteProfile?.title) return null;
196 return data;
197 },
198 async (params) => {
199 checkIn({ stage: 'get_images' });
200 return await handleSiteImages(params);
201 },
202 );
203
204 // needs: siteProfile
205 // provides: launchDecisions: { navExtras, navButtonLabel }
206 useRunStep(
207 'launchDecisions',
208 () => {
209 if (!data.siteProfile?.title) return null;
210 return data;
211 },
212 async (params) => {
213 checkIn({ stage: 'get_launch_decisions' });
214 return await handleLaunchDecisions(params);
215 },
216 );
217
218 // needs: siteProfile, sitePlugins, siteStyle, siteImages, aiHeaders
219 // provides: home: { id, slug, patterns, siteStyle }
220 useRunStep(
221 'home',
222 () => {
223 const ok = [
224 data.siteProfile,
225 data.siteStyle,
226 data.siteImages,
227 data.sitePlugins,
228 data.aiHeaders,
229 ].every((v) => v !== undefined);
230 return ok ? data : null;
231 },
232 async (params) => {
233 checkIn({ stage: 'get_home' });
234 return await handleHome(params);
235 },
236 );
237
238 // needs: siteProfile, sitePlugins, siteStyle, siteImages
239 // provides: pages: [{ id, slug, name, patterns, siteStyle }]
240 useRunStep(
241 'pages',
242 () => {
243 const ok = [
244 data.siteProfile,
245 data.siteStyle,
246 data.siteImages,
247 data.sitePlugins,
248 ].every((v) => v !== undefined);
249 return ok ? data : null;
250 },
251 async (params) => {
252 checkIn({ stage: 'get_pages' });
253 return await handlePages(params);
254 },
255 );
256
257 // basic defaults
258 useRunStep(
259 'generalUpdates',
260 () => {
261 const ok = [data.siteProfile, data.home, data.pages].every(
262 (v) => v !== undefined,
263 );
264 return ok ? data : null;
265 },
266 async ({ siteProfile, sitePlugins, siteStyle }) => {
267 checkIn({ stage: 'set_general', siteProfile, sitePlugins, siteStyle });
268
269 const { title } = siteProfile;
270 // translators: this is for a action log UI. Keep it short
271 addStatusMessage(launchStrings().statusAdmin);
272 await updateOption('permalink_structure', '/%postname%/');
273 setUserGaveConsent(true);
274 if (title) await updateOption('blogname', title);
275 },
276 );
277
278 // basic defaults
279 useRunStep(
280 'pluginConfigurations',
281 () => {
282 if (!data.sitePlugins) return null;
283 return data;
284 },
285 async () => {
286 checkIn({ stage: 'set_plugin_config' });
287 const activePlugins = await getActivePlugins();
288 if (alreadyActive(activePlugins, 'wpforms-lite')) {
289 await updateOption('wpforms_activation_redirect', 'skip');
290 }
291 if (alreadyActive(activePlugins, 'all-in-one-seo-pack')) {
292 await updateOption('aioseo_activation_redirect', 'skip');
293 }
294 if (alreadyActive(activePlugins, 'google-analytics-for-wordpress')) {
295 const param = '_transient__monsterinsights_activation_redirect';
296 await updateOption(param, null);
297 }
298 },
299 );
300
301 // If we have home and (maybe) pages then we're ready
302 useEffect(() => {
303 if (needToStall()) return;
304 const {
305 home,
306 pages,
307 siteProfile,
308 sitePlugins,
309 siteStyle,
310 aiBlogTitles,
311 siteImages,
312 designBuild,
313 launchDecisions,
314 } = data;
315 // pages could be [] and pass here, that's ok
316 if (!home || !pages) return;
317 if (homeStretch.current) return;
318 homeStretch.current = true;
319 (async () => {
320 const { objective, structure, category } = siteProfile;
321 const builtHome = designBuild?.builtPages?.find((p) => p.slug === 'home');
322 // Must match get-home.js's full-page test; if they drift, the home is
323 // built one way and planted another.
324 const isSinglePageDesign =
325 structure === 'single-page' &&
326 Boolean(builtHome?.fullPage && builtHome.patterns?.length) &&
327 Boolean(designBuild?.pages?.length);
328 const imageUrls = siteImageUrls(siteImages);
329 const intendedPlugins = (sitePlugins ?? []).map(
330 ({ wordpressSlug }) => wordpressSlug,
331 );
332
333 // Guarantee plugins are active before the pattern imports below rely on them.
334 await ensurePluginsActive(intendedPlugins);
335
336 const needsImprint = Array.isArray(showImprint)
337 ? showImprint.includes(wpLanguage ?? '') && category === 'Business'
338 : false;
339
340 const customFonts =
341 siteStyle?.variation?.settings?.typography?.fontFamilies?.custom;
342 let variation = siteStyle?.variation;
343 if (customFonts?.length) {
344 checkIn({ stage: 'install_fonts' });
345 // translators: this is for a action log UI. Keep it short
346 addStatusMessage(launchStrings().statusFonts);
347 const installed = await installFontFamilies(customFonts).catch(
348 () => [],
349 );
350 variation = mergeFontsIntoVariation(siteStyle.variation, installed);
351 }
352
353 if (siteStyle?.vibe && siteStyle.vibe !== 'natural-1') {
354 // translators: vibe in this context is a noun - the feeling of their site design.
355 addStatusMessage(launchStrings().statusSiteStyle);
356 checkIn({ stage: 'compute_vibe' });
357 const vibe = await computeVibeAdjustments(
358 siteStyle.vibe,
359 variation,
360 ).catch(() => null);
361 if (vibe) variation = { ...variation, ...vibe };
362 }
363
364 checkIn({ stage: 'set_vibe' });
365 await updateVariation(variation);
366
367 // navigation menu
368 addStatusMessage(launchStrings().statusNavigation);
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(launchStrings().statusLanding);
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(launchStrings().statusCreatingPages);
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 const heroDescription = heroDesc || data.heroDescription;
465 setData('heroDescription', heroDescription);
466 if (heroDescription) {
467 await updateOption('extendify_hero_description', heroDescription).catch(
468 () => null,
469 );
470 }
471
472 const createdPagesWP = await createWpPages(customPages, {
473 skipSectionIds: isSinglePageDesign,
474 });
475 // Aux pages
476 const blogPattern = home?.patterns?.find((pattern) =>
477 pattern.patternTypes.includes('blog-section'),
478 );
479 if (siteProfile.blog || blogPattern) {
480 checkIn({ stage: 'create_blog_sample_data' });
481 // translators: this is for a action log UI. Keep it short
482 addStatusMessage(launchStrings().statusBlog);
483 await createBlogSampleData(
484 { aiBlogTitles },
485 imageUrls,
486 blogPattern?.blogImages,
487 );
488 }
489 if (imageUrls.length) {
490 checkIn({ stage: 'set_hello_world_image' });
491 await setHelloWorldFeaturedImage(imageUrls);
492 }
493
494 let imprint = {};
495 if (needsImprint) {
496 checkIn({ stage: 'create_imprint' });
497 imprint = await addImprintPage({ siteStyle }).catch(() => null);
498 }
499
500 const pluginPages = [];
501 if (alreadyActive(activePlugins, 'woocommerce')) {
502 checkIn({ stage: 'import_woocommerce_products' });
503 addStatusMessage(
504 // translators: this is for a action log UI. Keep it short
505 launchStrings().statusStore,
506 );
507 await apiFetchWithTimeout({
508 path: '/extendify/v1/auto-launch/import-woocommerce',
509 }).catch(() => null);
510 const id = await getOption('woocommerce_shop_page_id');
511 const shopPage = id ? await getPageById(id) : null;
512 if (shopPage) pluginPages.push(shopPage);
513 }
514 if (alreadyActive(activePlugins, 'the-events-calendar')) {
515 pluginPages.push({
516 title: { rendered: __('Events', 'extendify-local') },
517 slug: 'events',
518 link: `${homeUrl}/events`,
519 });
520 }
521
522 // The design's page list omits the posts page like it omits shop, so the
523 // nav needs it here — but it's ours to create, not a PLUGIN_OWNED_PAGES.
524 pluginPages.push(...createdPagesWP.filter(isBlogPage));
525
526 // Adding pages to the nav
527 checkIn({ stage: 'set_page_links' });
528 let linksResult = { wpPages: createdPagesWP, headerCode };
529 if (!isSinglePageDesign) {
530 linksResult =
531 structure === 'single-page'
532 ? await updateSinglePageLinksToSections(
533 createdPagesWP,
534 customPages,
535 {
536 objective,
537 activePlugins,
538 landingPageCTALink: siteProfile.landingPageCTALink,
539 },
540 headerCode,
541 )
542 : await updateButtonLinks(createdPagesWP, pluginPages, headerCode);
543 }
544 const pagesWithLinksUpdated = linksResult.wpPages;
545 headerCode = linksResult.headerCode;
546 if (alreadyActive(activePlugins, 'woocommerce')) {
547 headerCode = injectWooCommerceIcons(headerCode);
548 }
549 await updateTemplatePart('extendable/header', headerCode);
550 const footerNavPages = [];
551 if (footerNavId && imprint?.title) {
552 const { originalSlug, title } = imprint;
553 footerNavPages.push({
554 id: originalSlug,
555 name: title.rendered,
556 slug: originalSlug,
557 patterns: [],
558 });
559 }
560
561 checkIn({ stage: 'set_navigation_links' });
562 if (objective !== 'landing-page') {
563 const orderedSlugs = designBuild?.pages?.map((p) => p.slug) ?? [];
564 // The tag only exists where the sections came back matched 1:1 to our list;
565 // read it off what was planted, so menu and section ids can't disagree.
566 const designOwnsNav =
567 isSinglePageDesign ||
568 Boolean(homePage?.patterns?.some(({ navSlug }) => navSlug));
569 if (designOwnsNav) {
570 await addSectionLinksFromDesign(
571 headerNavId,
572 designBuild.pages,
573 pluginPages,
574 );
575 } else if (structure === 'single-page') {
576 await addSectionLinksToNav(
577 headerNavId,
578 home?.patterns,
579 pluginPages,
580 createdPagesWP,
581 { orderedSlugs },
582 );
583 } else {
584 await addPageLinksToNav(
585 headerNavId,
586 pagesToCreate,
587 pagesWithLinksUpdated,
588 pluginPages,
589 { orderedSlugs },
590 );
591 }
592 if (footerNavId) {
593 await addPageLinksToNav(
594 footerNavId,
595 footerNavPages,
596 imprint?.id
597 ? [...pagesWithLinksUpdated, imprint]
598 : pagesWithLinksUpdated,
599 [],
600 );
601 }
602 }
603
604 checkIn({ stage: 'prefetch_assist_data' });
605 await prefetchAssistData();
606 checkIn({ stage: 'final_steps' });
607 await setThemeRenderingMode('template-locked');
608 await postLaunchFunctions();
609 if (imageUrls.length) {
610 await storeSiteImages(siteImages).catch(() => null);
611 }
612 // translators: this is for a action log UI. Keep it short
613 addStatusMessage(launchStrings().statusDone);
614 await Promise.all([
615 reportInactivePlugins(intendedPlugins).catch(() => null),
616 reportFailedDependencies(pagesReplaced),
617 checkIn({ stage: 'finished', siteProfile, sitePlugins, siteStyle }),
618 ]);
619 setWarnOnReload(false);
620 setDone(true);
621 })().catch((error) => {
622 console.error(error);
623 digest({
624 error,
625 details: { source: 'auto-launch', caller: 'create-site' },
626 });
627 // if we error here we can try again by resetting the home stretch and stalling again to refetch data
628 homeStretch.current = false;
629 needToStall(true);
630 setErrorMessage(launchStrings().errorFinalSteps);
631 });
632 }, [data, needToStall, setUserGaveConsent]);
633
634 return { done };
635 };
636
637 const useRunStep = (stepKey, getParams, fetcher) => {
638 const { setData, setErrorMessage, needToStall } = useLaunchDataStore();
639 const p = getParams?.() ?? null;
640 const { data, error } = useSWRImmutable(
641 p && !needToStall() ? stepKey : null,
642 () => fetcher(getParams()),
643 );
644
645 useEffect(() => {
646 if (!data) return;
647 Object.entries(data).forEach(([k, v]) => {
648 setData(k, v);
649 });
650 }, [data, setData]);
651
652 useEffect(() => {
653 if (!error || needToStall()) return;
654 console.error(error);
655 digest({ error, details: { source: 'auto-launch', caller: 'run-step' } });
656 setErrorMessage(launchStrings().errorStep);
657 }, [error, setErrorMessage, needToStall]);
658 };
659