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