PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
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 / Launch / lib / wp.js

wp.js in Extendify 3.1.4, at src/Launch/lib/wp.js

479 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import blogSampleData from '@launch/_data/blog-sample.json';
2 import {
3 generateCustomPatterns,
4 getImprintPageTemplate,
5 } from '@launch/api/DataApi';
6 import {
7 createCategory,
8 createPage,
9 createPost,
10 createTag,
11 getActivePlugins,
12 getThemeGlobalStyles,
13 processPlaceholders,
14 updateGlobalStyles,
15 updateOption,
16 updateThemeVariation,
17 uploadMedia,
18 } from '@launch/api/WPApi';
19 import { addIdAttributeToBlock } from '@launch/lib/blocks';
20 import { recordPluginActivity } from '@shared/api/DataApi';
21 import { pageNames } from '@shared/lib/pages';
22 import apiFetch from '@wordpress/api-fetch';
23 import { rawHandler, serialize } from '@wordpress/blocks';
24 import { __, sprintf } from '@wordpress/i18n';
25
26 // Currently this only processes patterns with placeholders
27 // by swapping out the placeholders with the actual code
28 // returns the patterns as blocks with the placeholders replaced
29 export const replacePlaceholderPatterns = async (patterns) => {
30 // Directly replace "blog-section" patterns using their replacement code, skipping the API call
31 patterns = patterns.map((pattern) => {
32 if (
33 pattern.patternTypes.includes('blog-section') &&
34 pattern.patternReplacementCode
35 ) {
36 return {
37 ...pattern,
38 code: pattern.patternReplacementCode,
39 };
40 }
41 return pattern;
42 });
43
44 const hasPlaceholders = patterns.filter((p) => p.patternReplacementCode);
45 if (!hasPlaceholders?.length) return patterns;
46
47 const activePlugins =
48 (await getActivePlugins())?.data?.map((path) => path.split('/')[0]) || [];
49
50 const pluginsActivity = patterns
51 .filter((p) => p.pluginDependency)
52 .map((p) => p.pluginDependency)
53 .filter((p) => !activePlugins.includes(p));
54
55 for (const plugin of pluginsActivity) {
56 recordPluginActivity({
57 slug: plugin,
58 source: 'launch',
59 });
60 }
61
62 try {
63 return await processPlaceholders(patterns);
64 } catch (_e) {
65 // Try one more time (plugins installed may not be fully loaded)
66 return await processPlaceholders(patterns)
67 // If this fails, just return the original patterns
68 .catch(() => patterns);
69 }
70 };
71
72 export const createWpPages = async (pages, { stickyNav }) => {
73 const pageIds = [];
74
75 for (const page of pages) {
76 const HTML = page.patterns.map(({ code }) => code).join('');
77 const blocks = rawHandler({ HTML });
78
79 const content = [];
80 // Use this to avoid adding duplicate Ids to patterns
81 const seenPatternTypes = new Set();
82 // Loop over every
83 for (const [i, pattern] of blocks.entries()) {
84 const patternType = page.patterns[i]?.patternTypes?.[0];
85 const serializedBlock = serialize(pattern);
86 // Get the translated slug
87 const { slug } =
88 Object.values(pageNames).find(({ alias }) =>
89 alias.includes(patternType),
90 ) || {};
91
92 // If we've already seen this slug, or no slug found, return the pattern unchanged
93 if (seenPatternTypes.has(slug) || !slug) {
94 content.push(serializedBlock);
95 continue;
96 }
97 // Add the slug to the seen list so we don't add it again
98 seenPatternTypes.add(slug);
99
100 content.push(addIdAttributeToBlock(serializedBlock, slug));
101 }
102
103 const pageData = {
104 title: page.name,
105 status: 'publish',
106 content: content.join(''),
107 template: stickyNav
108 ? 'no-title-sticky-header'
109 : page.slug === 'home'
110 ? 'no-title'
111 : 'page-with-title',
112 meta: { made_with_extendify_launch: true },
113 };
114 let newPage;
115 try {
116 newPage = await createPage(pageData);
117 } catch (_e) {
118 // The above could fail is they are on extendable < 2.0.12
119 // TODO: can remove in a month or so
120 pageData.template = 'no-title';
121 newPage = await createPage(pageData);
122 }
123 pageIds.push({ ...newPage, originalSlug: page.slug });
124 }
125
126 // When we have home, set reading setting
127 const maybeHome = pageIds.find(({ originalSlug }) => originalSlug === 'home');
128 if (maybeHome) {
129 await updateOption('show_on_front', 'page');
130 await updateOption('page_on_front', maybeHome.id);
131 }
132
133 // When we have blog, set reading setting
134 const maybeBlog = pageIds.find(({ originalSlug }) => originalSlug === 'blog');
135 if (maybeBlog) {
136 await updateOption('page_for_posts', maybeBlog.id);
137 }
138
139 return pageIds;
140 };
141
142 export const createWpCategories = async (categories) => {
143 const responses = [];
144 for (const category of categories) {
145 const categoryData = {
146 name: category.name,
147 slug: category.slug,
148 description: category.description,
149 };
150 let newCategory;
151 try {
152 newCategory = await createCategory(categoryData);
153 } catch (_e) {
154 // Fail silently
155 }
156 if (newCategory?.id && newCategory?.slug) {
157 responses.push({ id: newCategory.id, slug: newCategory.slug });
158 }
159 }
160 return responses;
161 };
162
163 export const createWpTags = async (tags) => {
164 const responses = [];
165 for (const tag of tags) {
166 const tagData = {
167 name: tag.name,
168 slug: tag.slug,
169 description: tag.description,
170 };
171 let newTag;
172 try {
173 newTag = await createTag(tagData);
174 } catch (_e) {
175 // Fail silently
176 }
177 if (newTag?.id && newTag?.slug) {
178 responses.push({ id: newTag.id, slug: newTag.slug });
179 }
180 }
181 return responses;
182 };
183
184 export const importImage = async (imageUrl, metadata) => {
185 try {
186 const loadImage = (img) => {
187 return new Promise((resolve, reject) => {
188 img.onload = () => resolve();
189 img.onerror = () => reject(new Error('Failed to load image.'));
190 });
191 };
192
193 const image = new Image();
194 image.src = imageUrl;
195 image.crossOrigin = 'anonymous';
196 await loadImage(image);
197
198 const canvas = document.createElement('canvas');
199 canvas.width = image.width;
200 canvas.height = image.height;
201
202 const ctx = canvas.getContext('2d');
203 if (!ctx) return null; // Fail silently
204
205 ctx.drawImage(image, 0, 0);
206
207 const blob = await new Promise((resolve, reject) => {
208 canvas.toBlob((blob) => {
209 if (blob) resolve(blob);
210 else reject(new Error('Failed to convert canvas to Blob.'));
211 }, 'image/jpeg');
212 });
213
214 const formData = new FormData();
215 formData.append(
216 'file',
217 new File([blob], metadata.filename, { type: 'image/jpeg' }),
218 );
219 formData.append('alt_text', metadata.alt || '');
220 formData.append('caption', metadata.caption || '');
221 formData.append('status', 'publish');
222
223 return await uploadMedia(formData);
224 } catch (_error) {
225 // Fail silently, return null
226 return null;
227 }
228 };
229
230 export const createBlogSampleData = async (siteStrings, siteImages) => {
231 const localizedBlogSampleData =
232 blogSampleData[window.extSharedData?.wpLanguage || 'en_US'] ||
233 blogSampleData.en_US;
234
235 const categories =
236 (await createWpCategories(localizedBlogSampleData.categories)) || [];
237 const tags = (await createWpTags(localizedBlogSampleData.tags)) || [];
238 const formatImageUrl = (image) =>
239 image?.includes('?q=80&w=1470') ? image : `${image}?q=80&w=1470`;
240 const imagesArray = (siteImages?.siteImages || []).sort(
241 () => Math.random() - 0.5,
242 );
243
244 const replacePostContentImages = (content, images) =>
245 (content.match(/https:\/\/images\.unsplash\.com\/[^\s"]+/g) || []).reduce(
246 (updated, match, i) =>
247 updated.replace(match, formatImageUrl(images[i] || match)),
248 content,
249 );
250
251 const posts = Array.from({ length: 8 }, (_, i) => {
252 const title =
253 siteStrings?.aiBlogTitles?.[i] ||
254 // translators: %s is a post number
255 sprintf(__('Blog Post %s', 'extendify-local'), i + 1);
256 const featuredImage = imagesArray[i % imagesArray.length]
257 ? formatImageUrl(imagesArray[i % imagesArray.length])
258 : null;
259 return {
260 name: title,
261 featured_image: featuredImage,
262 post_content: replacePostContentImages(
263 localizedBlogSampleData.post_content,
264 imagesArray,
265 ),
266 };
267 });
268
269 for (const [index, post] of posts.entries()) {
270 try {
271 const mediaId = post.featured_image
272 ? (
273 await importImage(post.featured_image, {
274 alt: '',
275 filename: `featured-image-${index}.jpg`,
276 caption: '',
277 })
278 )?.id || null
279 : null;
280
281 const category = categories.length
282 ? categories[index % categories.length]?.id
283 : [];
284
285 const tagFeaturedPost =
286 index < 4
287 ? [tags.find((tag) => tag.slug === 'featured')?.id].filter(Boolean)
288 : [];
289
290 const postData = {
291 title: post.name,
292 content: post.post_content,
293 status: 'publish',
294 featured_media: mediaId || null,
295 categories: category,
296 tags: tagFeaturedPost,
297 meta: { made_with_extendify_launch: true },
298 };
299
300 await createPost(postData);
301 } catch (_error) {
302 // Fail silently
303 }
304 }
305 };
306
307 export const setHelloWorldFeaturedImage = async (imageUrls) => {
308 try {
309 const translatedSlug = window.extOnbData?.helloWorldPostSlug;
310 let posts = await apiFetch({ path: `wp/v2/posts?slug=${translatedSlug}` });
311 if (!posts.length) {
312 posts = await apiFetch({ path: 'wp/v2/posts?slug=hello-world' });
313 }
314 if (!posts.length) return;
315 const helloPost = posts[0];
316 if (helloPost.featured_media && parseInt(helloPost.featured_media, 10) > 0)
317 return;
318 if (!Array.isArray(imageUrls) || imageUrls.length === 0) {
319 console.error('No image URLs provided.');
320 return;
321 }
322 const lastImageUrl = imageUrls[imageUrls.length - 1];
323 const mediaResponse = await importImage(lastImageUrl, {
324 alt: __('Hello World Featured Image', 'extendify-local'),
325 filename: 'hello-world-featured.jpg',
326 caption: '',
327 });
328 if (!mediaResponse || !mediaResponse.id) {
329 console.error('Image upload failed.');
330 return;
331 }
332 await apiFetch({
333 path: `wp/v2/posts/${helloPost.id}`,
334 method: 'POST',
335 data: { featured_media: mediaResponse.id },
336 });
337 } catch (error) {
338 console.error('Failed to set Hello World featured image:', error);
339 }
340 };
341
342 export const generateCustomPageContent = async (
343 pages,
344 userState,
345 siteProfile,
346 ) => {
347 // No ai-generated content
348 if (!siteProfile.description || !siteProfile.aiDescription) {
349 return pages;
350 }
351
352 const { siteId, partnerId, wpLanguage, wpVersion } = window.extSharedData;
353
354 const result = await Promise.allSettled(
355 pages.map((page) =>
356 generateCustomPatterns(
357 page,
358 {
359 ...userState,
360 siteId,
361 partnerId,
362 siteVersion: wpVersion,
363 language: wpLanguage,
364 },
365 siteProfile,
366 )
367 .then((response) => response)
368 .catch(() => page),
369 ),
370 );
371
372 return result?.map((page, i) => page.value || pages[i]);
373 };
374
375 export const updateGlobalStyleVariant = (variation) =>
376 updateThemeVariation(window.extSharedData.globalStylesPostID, variation);
377
378 export const addImprintPage = async (siteStyle) => {
379 try {
380 // Get the imprint page template
381 const imprintPage = await getImprintPageTemplate(siteStyle);
382
383 // Create the page in WordPress with the fetched template
384 const [createdImprintPage] = await createWpPages([imprintPage], {
385 stickyNav: false,
386 });
387
388 return createdImprintPage;
389 } catch (error) {
390 console.error('Failed to add imprint page:', error);
391 return null;
392 }
393 };
394
395 /**
396 * Updates natural-1 block style variations with selected vibe styles
397 * @param {string} selectedVibe - The vibe to apply (e.g., 'organic-1', 'bold-1')
398 */
399 export const updateNaturalVibeStyles = async (selectedVibe) => {
400 if (
401 !selectedVibe ||
402 typeof selectedVibe !== 'string' ||
403 selectedVibe.trim() === '' ||
404 selectedVibe === 'natural-1'
405 ) {
406 return;
407 }
408
409 const generateSourceStyleName = (naturalStyleName, targetVibe) =>
410 naturalStyleName.replace('--natural-1--', `--${targetVibe}--`);
411
412 const processBlockVariations = (variations, targetVibe) =>
413 Object.fromEntries(
414 Object.entries(variations).map(([styleName, styleProperties]) => {
415 if (!styleName.includes('--natural-1--')) {
416 return [styleName, { ...styleProperties }];
417 }
418
419 const sourceStyleName = generateSourceStyleName(styleName, targetVibe);
420 const sourceStyle = variations[sourceStyleName];
421
422 return [
423 styleName,
424 sourceStyle ? { ...sourceStyle } : { ...styleProperties },
425 ];
426 }),
427 );
428
429 try {
430 const globalStylesPostID = window.extSharedData?.globalStylesPostID;
431
432 if (!globalStylesPostID) {
433 throw new Error('Global styles post ID not found');
434 }
435
436 // Fetch theme styles
437 const { styles: themeStyles } = await getThemeGlobalStyles();
438
439 if (!themeStyles?.blocks) {
440 throw new Error('No block styles found in theme global styles');
441 }
442
443 // Process blocks with variations
444 const updatedBlocks = Object.fromEntries(
445 Object.entries(themeStyles.blocks).map(([blockName, blockObj]) => {
446 if (!blockObj?.variations) {
447 return [blockName, blockObj];
448 }
449
450 const { variations, ...rest } = blockObj;
451 const hasNaturalVariations = Object.keys(variations).some((styleName) =>
452 styleName.includes('--natural-1--'),
453 );
454
455 if (!hasNaturalVariations) {
456 return [blockName, blockObj];
457 }
458
459 return [
460 blockName,
461 {
462 ...rest,
463 variations: processBlockVariations(variations, selectedVibe),
464 },
465 ];
466 }),
467 );
468
469 // Apply the update
470 await updateGlobalStyles(globalStylesPostID, {
471 styles: { ...themeStyles, blocks: updatedBlocks },
472 });
473 } catch (error) {
474 const errorMessage =
475 error?.response?.data?.message || error?.message || 'Unknown error';
476 throw new Error(`Vibe update failed: ${errorMessage}`);
477 }
478 };
479