Setting the file. One moment.
Item SEO Tags Verify · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page scripts/ item-seo-tags-verify.js
JavaScript · 210 lines · 8 KB
// node skills/wix-replatform/scripts/item-seo-tags-verify.js \
15 // --wp-env <path to source.wordpress.env> \
16 // --wix-env <path to target.wix.env> \
17 // --wp-post-id <numeric WordPress post id already seeded with an override> \
18 // [--keep] # skip cleanup, print the retained itemId instead (debugging only)
19
20 const { readEnvFile } = require ( '../lib/config-env.js' );
21 const {
22 createWixClient ,
23 createDraftPost ,
24 publishDraftPost ,
25 deleteDraftPost ,
26 listMembers ,
27 } = require ( '../resources/rp-target-wix/lib/wix-writers.js' );
28
29 const WIXAPIS = 'https://www.wixapis.com' ;
30
31 function usage () {
32 return [
33 'Usage: node skills/wix-replatform/scripts/item-seo-tags-verify.js' ,
34 ' --wp-env <path> source.wordpress.env (WP_BASE_URL, WP_USERNAME, WP_APPLICATION_PASSWORD)' ,
35 ' --wix-env <path> target.wix.env (WIX_SITE_ID, WIX_API_KEY)' ,
36 ' --wp-post-id <id> WordPress post id already seeded with a genuine Yoast override' ,
37 ' [--keep] skip cleanup; print the retained itemId instead of deleting it' ,
38 ]. join ( ' \n ' );
39 }
40
41 function parseArgs ( argv ) {
42 const args = { keep: false };
43 for ( let i = 0 ; i < argv. length ; i += 1 ) {
44 const token = argv[i];
45 if (token === '--wp-env' ) args.wpEnvFile = argv[ ++ i];
46 else if (token === '--wix-env' ) args.wixEnvFile = argv[ ++ i];
47 else if (token === '--wp-post-id' ) args.wpPostId = argv[ ++ i];
48 else if (token === '--keep' ) args.keep = true ;
49 else throw new Error ( `Unexpected argument: ${ token }` );
50 }
51 if ( ! args.wpEnvFile || ! args.wixEnvFile || ! args.wpPostId) {
52 throw new Error ( `Missing required argument(s). \n\n ${ usage () }` );
53 }
54 return args;
55 }
56
57 // Same mapping as documented in item-seo-tags.json's mappingGuidance and already dry-run
58 // tested against poratus.wpcomstaging.com: title/description/og:*/twitter:card carry over;
59 // canonical, robots, and schema are deliberately dropped (source-domain / source-artefact /
60 // source-shaped, never passed through verbatim — see wordpress-seo.json's pitfalls).
61 function mapYoastHeadToItemSeoTags ( head , focusKeyword ) {
62 const tags = [];
63 if (head.title) tags. push ({ type: 'title' , children: head.title });
64 if (head.description) tags. push ({ type: 'meta' , props: { name: 'description' , content: head.description } });
65 for ( const [ key , prop ] of [
66 [ 'og_title' , 'og:title' ],
67 [ 'og_description' , 'og:description' ],
68 [ 'og_type' , 'og:type' ],
69 [ 'og_url' , 'og:url' ],
70 ]) {
71 if (head[key]) tags. push ({ type: 'meta' , props: { property: prop, content: head[key] } });
72 }
73 if (Array. isArray (head.og_image) && head.og_image[ 0 ]?.url) {
74 tags. push ({ type: 'meta' , props: { property: 'og:image' , content: head.og_image[ 0 ].url } });
75 }
76 if (head.twitter_card) tags. push ({ type: 'meta' , props: { name: 'twitter:card' , content: head.twitter_card } });
77 const focusKeywords = focusKeyword ? [{ term: focusKeyword, isMain: true }] : [];
78 return { tags, focusKeywords };
79 }
80
81 async function fetchWpPost ( wpBaseUrl , wpUser , wpAppPassword , postId ) {
82 const auth = 'Basic ' + Buffer. from ( `${ wpUser }:${ wpAppPassword . replace ( / \s + / g , '' ) }` ). toString ( 'base64' );
83 const res = await fetch ( `${ wpBaseUrl . replace ( / \/ $ / , '' ) }/wp-json/wp/v2/posts/${ postId }?context=edit` , {
84 headers: { Authorization: auth },
85 });
86 const text = await res. text ();
87 if ( ! res.ok) throw new Error ( `WordPress read of post ${ postId } failed: ${ res . status }: ${ text . slice ( 0 , 300 ) }` );
88 return JSON . parse (text);
89 }
90
91 function minimalRicosDocument ( text ) {
92 return {
93 nodes: [
94 {
95 type: 'PARAGRAPH' ,
96 id: '' ,
97 nodes: [{ type: 'TEXT' , id: '' , textData: { text, decorations: [] } }],
98 },
99 ],
100 };
101 }
102
103 async function bulkSetItemSeoTags ( wix , itemType , entries , { publish = false , returnEntity = true } = {}) {
104 return wix. send ({
105 method: 'POST' ,
106 url: `${ WIXAPIS }/promote/seo/v1/bulk/item-seo-tags/set` ,
107 body: { itemType, entries, publish, returnEntity },
108 });
109 }
110
111 async function getItemSeoTags ( wix , itemType , itemId ) {
112 return wix. send ({ method: 'GET' , url: `${ WIXAPIS }/promote/seo/v1/item-seo-tags/${ itemType }/${ itemId }` });
113 }
114
115 async function main () {
116 const args = parseArgs (process.argv. slice ( 2 ));
117 const wpEnv = await readEnvFile (args.wpEnvFile);
118 const wixEnv = await readEnvFile (args.wixEnvFile);
119
120 const wpPost = await fetchWpPost (wpEnv. WP_BASE_URL , wpEnv. WP_USERNAME , wpEnv. WP_APPLICATION_PASSWORD , args.wpPostId);
121 const focusKeyword = wpPost.meta?._yoast_wpseo_focuskw || null ;
122 if ( ! wpPost.meta?._yoast_wpseo_title && ! wpPost.meta?._yoast_wpseo_metadesc) {
123 throw new Error (
124 `WordPress post ${ args . wpPostId } has no genuine Yoast override (_yoast_wpseo_title/_metadesc are empty) — ` +
125 're-run scripts/test-site-seed/seed-plugin-data.mjs first, or pass an already-seeded post id.' ,
126 );
127 }
128 const mapped = mapYoastHeadToItemSeoTags (wpPost.yoast_head_json || {}, focusKeyword);
129 const wantedTitle = mapped.tags. find (( t ) => t.type === 'title' )?.children;
130
131 const wix = createWixClient ({ authToken: wixEnv. WIX_API_KEY , siteId: wixEnv. WIX_SITE_ID });
132
133 let draftPostId = null ;
134 let outcome = { ok: false };
135 try {
136 const memberList = await listMembers (wix, { limit: 1 });
137 const memberId = memberList?.members?.[ 0 ]?.id;
138 if ( ! memberId) {
139 throw new Error (
140 'no member found on the target site to author the throwaway post — the site owner \' s ' +
141 'auto-created user-member should normally be present (see wix-writers.js listMembers comment)' ,
142 );
143 }
144
145 const draft = await createDraftPost (wix, {
146 title: `[spec-0056 verification — safe to delete] ${ wpPost . slug }` ,
147 memberId,
148 richContent: minimalRicosDocument (
149 'Throwaway post created by item-seo-tags-verify.js (spec 0056) to live-verify the Bulk Set Item SEO Tags write path. Safe to delete.' ,
150 ),
151 });
152 draftPostId = draft.id;
153 await publishDraftPost (wix, draftPostId);
154
155 const setResult = await bulkSetItemSeoTags (
156 wix,
157 'BLOG_POST' ,
158 [
159 {
160 itemId: draftPostId,
161 itemSeoTags: { tags: mapped.tags, focusKeywords: mapped.focusKeywords },
162 fieldMask: [ 'tags' , 'focusKeywords' ],
163 },
164 ],
165 { returnEntity: true },
166 );
167
168 const entry = setResult.results?.[ 0 ];
169 if ( ! entry?.itemMetadata?.success) {
170 throw new Error ( `Bulk Set Item SEO Tags failed for the entry: ${ JSON . stringify ( entry ?. itemMetadata ?. error ) }` );
171 }
172
173 const readBack = await getItemSeoTags (wix, 'BLOG_POST' , draftPostId);
174 const resolvedTitle = (readBack.resolvedTags || []). find (( rt ) => rt.tag?.type === 'title' );
175 const titleLandedAsItemOverride = resolvedTitle?.source === 'TAG_SOURCE_ITEM' && resolvedTitle.tag?.children === wantedTitle;
176
177 outcome = {
178 ok: titleLandedAsItemOverride,
179 draftPostId,
180 wpPostId: args.wpPostId,
181 mappedTags: mapped.tags,
182 setResult,
183 readBack,
184 };
185 } finally {
186 if (draftPostId && ! args.keep) {
187 try {
188 await deleteDraftPost (wix, draftPostId, { permanent: true });
189 outcome.cleanup = 'deleted' ;
190 } catch (cleanupError) {
191 outcome.cleanup = 'FAILED' ;
192 outcome.retainedItemId = draftPostId;
193 console. error (
194 `CLEANUP FAILED — throwaway Wix Blog post retained on the target site, itemId=${ draftPostId }: ${ cleanupError . message }` ,
195 );
196 }
197 } else if (draftPostId) {
198 outcome.cleanup = 'skipped (--keep)' ;
199 outcome.retainedItemId = draftPostId;
200 }
201 }
202
203 console. log ( JSON . stringify (outcome, null , 2 ));
204 if ( ! outcome.ok) process.exitCode = 1 ;
205 }
206
207 main (). catch (( error ) => {
208 console. error (error.stack || error.message);
209 process. exit ( 1 );
210 });