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 / Launch / lib / wp.js

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

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