1'use strict';23// SEO tag rules for the seo/* writers : which tags an import may write, how a read is4// merged with what the import brings (every SEO write replaces in full, with no revision check),5// and how a read-back proves a write landed. Pure functions; the builders and executors are in6// wix-writers.js.7//8// robots is never written by an import, at site, pattern or item level. Indexing is a launch9// decision. A robots tag the target already has is kept verbatim, because leaving it out of a10// full-replacement write would change indexing just as surely as writing one.1112const ITEM_SEO_ITEM_TYPES = Object.freeze(['BLOG_POST']);
55 if (!tag || typeof tag !== 'object' || !TAG_TYPES.includes(tag.type)) throw new Error(`${where}: tag type must be one of ${TAG_TYPES.join(', ')}`);
56 if (tag.type === 'title' && (typeof tag.children !== 'string' || !tag.children.trim())) throw new Error(`${where}: a title tag needs non-empty children`);
57 if (tag.type === 'meta' && (!tag.props || !(tag.props.name || tag.props.property) || typeof tag.props.content !== 'string')) throw new Error(`${where}: a meta tag needs props.name or props.property and a string props.content`);
58 if (tag.type === 'link' && (!tag.props || !tag.props.rel || typeof tag.props.href !== 'string')) throw new Error(`${where}: a link tag needs props.rel and props.href`);
59 if (tag.type === 'script' && (!tag.props || !tag.props.type || typeof tag.children !== 'string')) throw new Error(`${where}: a script tag needs props.type and children`);
60}
61
62// Item level (item 4): robots and canonical are never written by an import. A source canonical is
63// either the page itself, which Wix generates, or an old-site URL, which is the redirect helper's job.
64// `preserved` are tags the item already has in Wix, which a full-replacement write must keep.
66 if (!Array.isArray(tags)) throw new Error('item SEO tags: tags must be an array');
67 if (tags.length > ITEM_SEO_MAX_TAGS) throw new Error(`item SEO tags: at most ${ITEM_SEO_MAX_TAGS} tags per item`);
68 tags.forEach((tag, i) => {
69 assertTagShape(tag, `item SEO tags[${i}]`);
70 if ((isRobots(tag) || isCanonical(tag)) && !keptFromTarget(tag, preserved)) throw new Error(`item SEO tags[${i}]: ${isRobots(tag) ? 'robots' : 'canonical'} is never written by an import`);
71 });
72}
73// What the executor takes out before a write, recorded as an exclusion rather than dropped.
74function splitItemExclusions(tags) {
75 const kept = [], exclusions = [];
76 for (const tag of tags || []) {
77 if (isRobots(tag)) exclusions.push({ reasonCode: 'item-robots-not-written', tag });
78 else if (isCanonical(tag)) exclusions.push({ reasonCode: 'source-canonical-not-written', tag });
79 else kept.push(tag);
80 }
81 return { tags: kept, exclusions };
82}
83
84// Site level: only the default share image and search-engine verification tags.
86 if (!Array.isArray(tags)) throw new Error('site SEO tags: tags must be an array');
87 const providers = new Set();
88 tags.forEach((tag, i) => {
89 const where = `site SEO tags[${i}]`;
90 if (keptFromTarget(tag, preserved)) return;
91 if (isRobots(tag)) throw new Error(`${where}: site-noindex-refused: robots is never written by an import (indexing is a launch decision)`);
92 assertTagShape(tag, where);
93 if (tag.type !== 'meta') throw new Error(`${where}: site SEO tags accept meta tags only`);
94 const id = tagIdentity(tag);
95 if (id === 'meta:property:og:image') {
96 if (!/^https:\/\/static\.wixstatic\.com\//.test(tag.props.content)) throw new Error(`${where}: og:image must be a Wix media URL (https://static.wixstatic.com/...)`);
97 return;
98 }
99 const name = lower(tag.props.name);
100 if (!SITE_VERIFICATION_NAMES.includes(name)) throw new Error(`${where}: only og:image and verification tags (${SITE_VERIFICATION_NAMES.join(', ')}) are imported at site level`);
101 if (providers.has(name)) throw new Error(`${where}: one ${name} tag per site`);
102 providers.add(name);
103 });
104}
105// An og:image tag passed in `tags` is never written: the share image comes only through
106// setSiteSeoTags' shareImage (the granted media id), so a URL from the mapping is never sent.
123 if (isRobots(tag) && !keptFromTarget(tag, preserved)) throw new Error(`SEO pattern tags[${i}]: robots is never written by an import (indexing is a launch decision)`);
124 });
125}
126function splitPatternExclusions(tags) {
127 const kept = [], exclusions = [];
128 for (const tag of tags || []) (isRobots(tag) ? exclusions.push({ reasonCode: 'pattern-robots-not-written', tag }) : kept.push(tag));
129 return { tags: kept, exclusions };
130}
131
132// A full-replacement write: keep every current tag whose identity the import does not bring, and
133// replace the identities it does. Robots on the target are always kept as they are.
134function mergeTags(current, incoming) {
135 const replaced = new Set((incoming || []).filter(tag => !isRobots(tag)).map(tagIdentity));
151 const terms = new Set(((itemSeoTags && itemSeoTags.focusKeywords) || []).map(k => k && k.term));
152 for (const keyword of focusKeywords) if (!terms.has(keyword.term)) failures.push({ identity: `focusKeyword:${keyword.term}`, resolvedSource: null });
190 action: `Decide in Business Manager > Marketing & SEO > SEO settings whether search engines may index the site.${refused ? ' The source asked for noindex; the import did not apply it.' : ''} The import never changes indexing.`,
191 deadlineReason: 'Indexing is a launch decision; settle it before the DNS switch.', resolutionEvidence: 'Operator confirmation in the cutover checklist.' });
195 action: 'The source SEO pattern replaced a pattern the site already had. Keep it, or restore the previous pattern (in the import result) in Business Manager.', deadlineReason: 'Before go-live sign-off.', resolutionEvidence: 'Operator confirmation in the cutover checklist.' });
198 action: 'These source SEO patterns were not written (unknown template variable, missing app, or unsupported page type). Set them by hand in Business Manager, or accept the Wix default.', deadlineReason: 'Before go-live sign-off.', resolutionEvidence: 'Pattern read in Business Manager, or operator acceptance.' });
202 action: `The source default social share image (og:image) was not written (${[...new Set(shareImages.map(e => e.cause || 'not-imported'))].sort().join(', ')}): its own image did not import, or the write did not read back. Set it in Business Manager > Marketing & SEO > SEO settings (social share image), using the source image listed here, or accept the Wix default.`,
203 deadlineReason: 'Before go-live sign-off: shared links show the Wix default image until then.', resolutionEvidence: 'Share image shown in Business Manager SEO settings, or operator acceptance.' });
208 action: 'Source robots and canonical tags were not carried over; Wix defaults apply. Old-URL redirects are handled separately (the redirect helper).', deadlineReason: 'Informational; no action needed before go-live.', resolutionEvidence: 'None required.' });