PluginProbe
Extendify / 2.2.0
Extendify v2.2.0
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 2.2.0, at src/Launch/lib/wp.js

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