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

wp.js in Extendify 3.1.5, at src/AutoLaunch/functions/wp.js

277 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Functions that interact with WordPress
2
3 import blogSampleData from '@launch/_data/blog-sample.json';
4 import apiFetch from '@wordpress/api-fetch';
5 import { __, sprintf } from '@wordpress/i18n';
6 import { addQueryArgs } from '@wordpress/url';
7
8 const allowedHeaders = [
9 'header-atlas-beacon',
10 'header-ember-harbor',
11 'header-catalina-skyline',
12 'header-ceadar-peak',
13 ];
14 const allowedFooters = [
15 'footer',
16 'footer-social-icons',
17 'footer-with-center-logo-and-menu',
18 ];
19 const allowedFootersWithNav = [
20 'footer-with-nav',
21 'footer-with-center-logo-social-nav',
22 ];
23
24 const MS_PER_DAY = 86_400_000;
25
26 export const updateOption = (option, value) =>
27 apiFetch({
28 path: '/extendify/v1/auto-launch/options',
29 method: 'POST',
30 data: { option, value },
31 });
32 export const getOption = (option) =>
33 apiFetch({
34 path: addQueryArgs(`/extendify/v1/auto-launch/options`, { option }),
35 });
36
37 export const storeSiteImages = (siteImages) =>
38 apiFetch({
39 path: '/extendify/v1/shared/site-images',
40 method: 'POST',
41 data: { siteImages },
42 });
43
44 export const clearSiteImages = () =>
45 apiFetch({
46 path: '/extendify/v1/shared/site-images/clear',
47 method: 'POST',
48 });
49
50 export const getPageById = (id) => {
51 try {
52 return apiFetch({ path: `/wp/v2/pages/${id}` });
53 } catch {
54 return null;
55 }
56 };
57
58 const getTemplateParts = () => apiFetch({ path: '/wp/v2/template-parts' });
59
60 export const getHeadersAndFooters = async ({ useNavFooter = false } = {}) => {
61 const patterns = await getTemplateParts();
62 const extendablePatterns = patterns.filter(
63 ({ theme }) => theme === 'extendable',
64 );
65 const headers = extendablePatterns?.filter(({ slug }) =>
66 allowedHeaders.includes(slug),
67 );
68
69 const footerNav =
70 useNavFooter &&
71 patterns?.some(({ slug }) => allowedFootersWithNav.includes(slug));
72 const footerSlugsToUse = footerNav ? allowedFootersWithNav : allowedFooters;
73
74 const footers = extendablePatterns.filter(({ slug }) =>
75 footerSlugsToUse.includes(slug),
76 );
77 return { headers, footers };
78 };
79
80 export const uploadMedia = (formData) =>
81 apiFetch({ path: 'wp/v2/media', body: formData, method: 'POST' });
82
83 export const importImage = async (imageUrl, metadata) => {
84 try {
85 const loadImage = (img) => {
86 return new Promise((resolve, reject) => {
87 img.onload = () => resolve();
88 img.onerror = () => reject(new Error('Failed to load image.'));
89 });
90 };
91
92 const image = new Image();
93 image.src = imageUrl;
94 image.crossOrigin = 'anonymous';
95 await loadImage(image);
96
97 const canvas = document.createElement('canvas');
98 canvas.width = image.width;
99 canvas.height = image.height;
100
101 const ctx = canvas.getContext('2d');
102 if (!ctx) return null; // Fail silently
103
104 ctx.drawImage(image, 0, 0);
105
106 const blob = await new Promise((resolve, reject) => {
107 canvas.toBlob((blob) => {
108 if (blob) resolve(blob);
109 else reject(new Error('Failed to convert canvas to Blob.'));
110 }, 'image/jpeg');
111 });
112
113 const formData = new FormData();
114 formData.append(
115 'file',
116 new File([blob], metadata.filename, { type: 'image/jpeg' }),
117 );
118 formData.append('alt_text', metadata.alt || '');
119 formData.append('caption', metadata.caption || '');
120 formData.append('status', 'publish');
121
122 return await uploadMedia(formData);
123 } catch (_error) {
124 // Fail silently, return null
125 return null;
126 }
127 };
128
129 export const createPost = (data) =>
130 apiFetch({ path: '/wp/v2/posts', method: 'POST', data });
131
132 export const createTag = (data) =>
133 apiFetch({ path: '/wp/v2/tags', method: 'POST', data });
134
135 export const createCategory = (data) =>
136 apiFetch({ path: '/wp/v2/categories', method: 'POST', data });
137
138 const formatImageUrl = (image) =>
139 image?.includes('?q=80&w=1470') ? image : `${image}?q=80&w=1470`;
140
141 const replacePostContentImages = (content, images) =>
142 (content.match(/https:\/\/images\.unsplash\.com\/[^\s"]+/g) || []).reduce(
143 (updated, match, i) =>
144 updated.replace(match, formatImageUrl(images[i] || match)),
145 content,
146 );
147
148 // The blog-section images fill the newest posts so the home's date-desc loop matches
149 // the preview 1:1; extra posts fall back to the shared pool.
150 export const buildBlogPosts = ({
151 blogTitles,
152 siteImages,
153 blogImages = [],
154 sampleData,
155 }) => {
156 const pool = siteImages || [];
157 // Every post shares the same sample body, so resolve its images once.
158 const postContent = replacePostContentImages(sampleData.post_content, pool);
159 return Array.from({ length: 8 }, (_, i) => {
160 const title =
161 blogTitles?.[i] ||
162 // translators: %s is a post number
163 sprintf(__('Blog Post %s', 'extendify-local'), i + 1);
164 const image = blogImages[i] ?? pool[i % pool.length];
165 return {
166 name: title,
167 featured_image: image ? formatImageUrl(image) : null,
168 post_content: postContent,
169 };
170 });
171 };
172
173 export const createBlogSampleData = async (
174 siteStrings,
175 siteImages,
176 blogImages,
177 ) => {
178 const localizedBlogSampleData =
179 blogSampleData[window.extSharedData?.wpLanguage || 'en_US'] ||
180 blogSampleData.en_US;
181
182 const categories =
183 (await createWpCategories(localizedBlogSampleData.categories)) || [];
184 const tags = (await createWpTags(localizedBlogSampleData.tags)) || [];
185
186 const posts = buildBlogPosts({
187 blogTitles: siteStrings?.aiBlogTitles,
188 siteImages,
189 blogImages,
190 sampleData: localizedBlogSampleData,
191 });
192
193 for (const [index, post] of posts.entries()) {
194 try {
195 const mediaId = post.featured_image
196 ? (
197 await importImage(post.featured_image, {
198 alt: '',
199 filename: `featured-image-${index}.jpg`,
200 caption: '',
201 })
202 )?.id || null
203 : null;
204
205 const category = categories.length
206 ? categories[index % categories.length]?.id
207 : [];
208
209 const tagFeaturedPost =
210 index < 4
211 ? [tags.find((tag) => tag.slug === 'featured')?.id].filter(Boolean)
212 : [];
213
214 const postData = {
215 title: post.name,
216 content: post.post_content,
217 status: 'publish',
218 // Space posts a day apart, newest first, so the home's date-desc loop
219 // matches the preview order.
220 date_gmt: new Date(Date.now() - index * MS_PER_DAY)
221 .toISOString()
222 .slice(0, 19),
223 featured_media: mediaId || null,
224 categories: category,
225 tags: tagFeaturedPost,
226 meta: { made_with_extendify_launch: true },
227 };
228
229 await createPost(postData);
230 } catch (_error) {
231 // Fail silently
232 }
233 }
234 };
235
236 export const createWpCategories = async (categories) => {
237 const responses = [];
238 for (const category of categories) {
239 const categoryData = {
240 name: category.name,
241 slug: category.slug,
242 description: category.description,
243 };
244 let newCategory;
245 try {
246 newCategory = await createCategory(categoryData);
247 } catch (_e) {
248 // Fail silently
249 }
250 if (newCategory?.id && newCategory?.slug) {
251 responses.push({ id: newCategory.id, slug: newCategory.slug });
252 }
253 }
254 return responses;
255 };
256
257 export const createWpTags = async (tags) => {
258 const responses = [];
259 for (const tag of tags) {
260 const tagData = {
261 name: tag.name,
262 slug: tag.slug,
263 description: tag.description,
264 };
265 let newTag;
266 try {
267 newTag = await createTag(tagData);
268 } catch (_e) {
269 // Fail silently
270 }
271 if (newTag?.id && newTag?.slug) {
272 responses.push({ id: newTag.id, slug: newTag.slug });
273 }
274 }
275 return responses;
276 };
277