PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 0.7.0 All 126 releases
extendify / src / AutoLaunch / hooks / useCreateSite.js

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

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