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

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