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

660 lines 20.0 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 { 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 vibe = await computeVibeAdjustments(
356 siteStyle.vibe,
357 variation,
358 ).catch(() => null);
359 if (vibe) variation = { ...variation, ...vibe };
360 }
361
362 checkIn({ stage: 'set_vibe' });
363 await updateVariation(variation);
364
365 // navigation menu
366 addStatusMessage(__('Working on the navigation', 'extendify-local'));
367 const { id: headerNavId } = await createNavigation({
368 title: __('Header Navigation', 'extendify-local'),
369 slug: 'site-navigation',
370 });
371 let headerCode = updateNavAttributes(home.headerCode || '', {
372 ref: headerNavId,
373 });
374 headerCode = injectNavExtras(headerCode, launchDecisions);
375 // remove the header navigation from the landing page
376 if (objective === 'landing-page') {
377 // translators: this is for a action log UI. Keep it short
378 addStatusMessage(__('Perfecting a landing page', 'extendify-local'));
379 const social =
380 /<!--\s*wp:social-links\b[^>]*>.*?<!--\s*\/wp:social-links\s*-->/gis;
381 headerCode = headerCode
382 .replace(/<!--\s*wp:navigation\b[^>]*.*\/-->/gis, '')
383 .replace(social, '');
384 }
385 headerCode = headerCode.replaceAll(
386 '206-555-0100',
387 (typeof siteProfile.phoneNumber === 'string' &&
388 siteProfile.phoneNumber) ||
389 // 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.
390 __('206-555-0100', 'extendify-local'),
391 );
392 checkIn({ stage: 'set_navigation' });
393
394 // footer
395 let footerNavId = null;
396 let footerCode = home.footerCode || '';
397 // The logo already carries the brand, so the site title would be redundant.
398 if (designBuild?.hasExternalLogo && footerCode.includes('wp:site-logo')) {
399 footerCode = footerCode.replace(
400 /\s*<!--\s*wp:site-title[\s\S]*?\/-->/g,
401 '',
402 );
403 }
404 if (needsImprint) {
405 const nav = await createNavigation({
406 title: __('Footer Navigation', 'extendify-local'),
407 slug: 'footer-navigation',
408 });
409 footerNavId = nav.id;
410 footerCode = updateNavAttributes(footerCode, { ref: footerNavId });
411 }
412 footerCode = applySocialProfiles(footerCode, siteProfile.socialProfiles);
413 checkIn({ stage: 'set_footer' });
414 await updateTemplatePart('extendable/footer', footerCode);
415
416 // pages
417 // translators: this is for a action log UI. Keep it short
418 addStatusMessage(__('Creating pages', 'extendify-local'));
419 const pagesToCreate = getPagesToCreate(data);
420 const titlePattern = pages?.[0]?.patterns?.find((p) =>
421 p.patternTypes?.includes('page-title'),
422 );
423 if (titlePattern) {
424 checkIn({ stage: 'set_page_title_pattern' });
425 await updatePageTitlePattern(titlePattern.code);
426 }
427
428 const activePlugins = await getActivePlugins();
429 // Keep plugin pages in the nav but skip creating the page itself.
430 const reservedSlugs = new Set(
431 PLUGIN_OWNED_PAGES.filter(
432 ({ plugin }) =>
433 sitePlugins.some((p) => p.wordpressSlug === plugin) ||
434 alreadyActive(activePlugins, plugin),
435 ).map(({ slug }) => slug),
436 );
437 const pagesToActuallyCreate = pagesToCreate.filter(
438 (p) => !reservedSlugs.has(p.slug),
439 );
440
441 // Some patterns have preview html, we can replace those
442 // which may install some plugins too.
443 const pagesReplaced = [];
444 // Run these one page at a time so we don't end up
445 // with duplicate dependency issues
446 checkIn({ stage: 'replace_placeholder_patterns' });
447 for (const page of pagesToActuallyCreate) {
448 const patterns = await replacePlaceholderPatterns(page.patterns);
449 const updatedPage = { ...page, patterns };
450 pagesReplaced.push(updatedPage);
451 }
452 checkIn({ stage: 'generate_page_content' });
453 const customPages = await generatePageContent(pagesReplaced, data);
454
455 // Update heroDescription to the actual AI-rewritten hero content
456 const homePage = customPages.find((p) => p.slug === 'home');
457 const heroPattern = homePage?.patterns?.find((p) =>
458 p.patternTypes?.includes('hero-header'),
459 );
460 const pMatch = heroPattern?.code?.match(/<p[^>]*>([\s\S]*?)<\/p>/);
461 const heroDesc = pMatch?.[1]?.replace(/<[^>]+>/g, '').trim();
462 setData('heroDescription', heroDesc || data.heroDescription);
463
464 const createdPagesWP = await createWpPages(customPages, {
465 skipSectionIds: isSinglePageDesign,
466 });
467 // Aux pages
468 const blogPattern = home?.patterns?.find((pattern) =>
469 pattern.patternTypes.includes('blog-section'),
470 );
471 if (siteProfile.blog || blogPattern) {
472 checkIn({ stage: 'create_blog_sample_data' });
473 // translators: this is for a action log UI. Keep it short
474 addStatusMessage(__('Creating blog sample data', 'extendify-local'));
475 await createBlogSampleData(
476 { aiBlogTitles },
477 imageUrls,
478 blogPattern?.blogImages,
479 );
480 }
481 if (imageUrls.length) {
482 checkIn({ stage: 'set_hello_world_image' });
483 await setHelloWorldFeaturedImage(imageUrls);
484 }
485
486 let imprint = {};
487 if (needsImprint) {
488 checkIn({ stage: 'create_imprint' });
489 imprint = await addImprintPage({ siteStyle }).catch(() => null);
490 }
491
492 const pluginPages = [];
493 if (alreadyActive(activePlugins, 'woocommerce')) {
494 checkIn({ stage: 'import_woocommerce_products' });
495 addStatusMessage(
496 // translators: this is for a action log UI. Keep it short
497 __('Setting up your online store', 'extendify-local'),
498 );
499 await apiFetchWithTimeout({
500 path: '/extendify/v1/auto-launch/import-woocommerce',
501 }).catch(() => null);
502 const id = await getOption('woocommerce_shop_page_id');
503 const shopPage = id ? await getPageById(id) : null;
504 if (shopPage) pluginPages.push(shopPage);
505 }
506 if (alreadyActive(activePlugins, 'the-events-calendar')) {
507 pluginPages.push({
508 title: { rendered: __('Events', 'extendify-local') },
509 slug: 'events',
510 link: `${homeUrl}/events`,
511 });
512 }
513
514 // The design's page list omits the posts page like it omits shop, so the
515 // nav needs it here — but it's ours to create, not a PLUGIN_OWNED_PAGES.
516 pluginPages.push(...createdPagesWP.filter(isBlogPage));
517
518 // Adding pages to the nav
519 checkIn({ stage: 'set_page_links' });
520 let linksResult = { wpPages: createdPagesWP, headerCode };
521 if (!isSinglePageDesign) {
522 linksResult =
523 structure === 'single-page'
524 ? await updateSinglePageLinksToSections(
525 createdPagesWP,
526 customPages,
527 {
528 objective,
529 activePlugins,
530 landingPageCTALink: siteProfile.landingPageCTALink,
531 },
532 headerCode,
533 )
534 : await updateButtonLinks(createdPagesWP, pluginPages, headerCode);
535 }
536 const pagesWithLinksUpdated = linksResult.wpPages;
537 headerCode = linksResult.headerCode;
538 if (alreadyActive(activePlugins, 'woocommerce')) {
539 headerCode = injectWooCommerceIcons(headerCode);
540 }
541 await updateTemplatePart('extendable/header', headerCode);
542 const footerNavPages = [];
543 if (footerNavId && imprint?.title) {
544 const { originalSlug, title } = imprint;
545 footerNavPages.push({
546 id: originalSlug,
547 name: title.rendered,
548 slug: originalSlug,
549 patterns: [],
550 });
551 }
552
553 checkIn({ stage: 'set_navigation_links' });
554 if (objective !== 'landing-page') {
555 const orderedSlugs = designBuild?.pages?.map((p) => p.slug) ?? [];
556 // The tag only exists where the sections came back matched 1:1 to our list;
557 // read it off what was planted, so menu and section ids can't disagree.
558 const designOwnsNav =
559 isSinglePageDesign ||
560 Boolean(homePage?.patterns?.some(({ navSlug }) => navSlug));
561 if (designOwnsNav) {
562 await addSectionLinksFromDesign(
563 headerNavId,
564 designBuild.pages,
565 pluginPages,
566 );
567 } else if (structure === 'single-page') {
568 await addSectionLinksToNav(
569 headerNavId,
570 home?.patterns,
571 pluginPages,
572 createdPagesWP,
573 { orderedSlugs },
574 );
575 } else {
576 await addPageLinksToNav(
577 headerNavId,
578 pagesToCreate,
579 pagesWithLinksUpdated,
580 pluginPages,
581 { orderedSlugs },
582 );
583 }
584 if (footerNavId) {
585 await addPageLinksToNav(
586 footerNavId,
587 footerNavPages,
588 imprint?.id
589 ? [...pagesWithLinksUpdated, imprint]
590 : pagesWithLinksUpdated,
591 [],
592 );
593 }
594 }
595
596 checkIn({ stage: 'prefetch_assist_data' });
597 await prefetchAssistData();
598 checkIn({ stage: 'final_steps' });
599 await setThemeRenderingMode('template-locked');
600 await postLaunchFunctions();
601 if (imageUrls.length) {
602 await storeSiteImages(siteImages).catch(() => null);
603 }
604 // translators: this is for a action log UI. Keep it short
605 addStatusMessage(__('All done!', 'extendify-local'));
606 await Promise.all([
607 reportInactivePlugins(intendedPlugins).catch(() => null),
608 checkIn({ stage: 'finished', siteProfile, sitePlugins, siteStyle }),
609 ]);
610 setWarnOnReload(false);
611 setDone(true);
612 })().catch((error) => {
613 console.error(error);
614 digest({
615 error,
616 details: { source: 'auto-launch', caller: 'create-site' },
617 });
618 // if we error here we can try again by resetting the home stretch and stalling again to refetch data
619 homeStretch.current = false;
620 needToStall(true);
621 setErrorMessage(
622 __(
623 'Something went wrong during the final steps. We will try again but you may need to refresh the page.',
624 'extendify-local',
625 ),
626 );
627 });
628 }, [data, needToStall, setUserGaveConsent]);
629
630 return { done };
631 };
632
633 const useRunStep = (stepKey, getParams, fetcher) => {
634 const { setData, setErrorMessage, needToStall } = useLaunchDataStore();
635 const p = getParams?.() ?? null;
636 const { data, error } = useSWRImmutable(
637 p && !needToStall() ? stepKey : null,
638 () => fetcher(getParams()),
639 );
640
641 useEffect(() => {
642 if (!data) return;
643 Object.entries(data).forEach(([k, v]) => {
644 setData(k, v);
645 });
646 }, [data, setData]);
647
648 useEffect(() => {
649 if (!error || needToStall()) return;
650 console.error(error);
651 digest({ error, details: { source: 'auto-launch', caller: 'run-step' } });
652 setErrorMessage(
653 __(
654 'Having some trouble with this step. Trying again...',
655 'extendify-local',
656 ),
657 );
658 }, [error, setErrorMessage, needToStall]);
659 };
660