@se-studio/site-check 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +14 -0
- package/dist/collect-markdown-urls.d.ts +4 -0
- package/dist/collect-markdown-urls.d.ts.map +1 -1
- package/dist/collect-markdown-urls.js +1 -1
- package/dist/collect-markdown-urls.js.map +1 -1
- package/dist/production-audit/index.d.ts +7 -0
- package/dist/production-audit/index.d.ts.map +1 -1
- package/dist/production-audit/index.js +32 -28
- package/dist/production-audit/index.js.map +1 -1
- package/package.json +1 -8
- package/dist/cms-seo/bulk-action-publish.d.ts +0 -56
- package/dist/cms-seo/bulk-action-publish.d.ts.map +0 -1
- package/dist/cms-seo/bulk-action-publish.js +0 -488
- package/dist/cms-seo/bulk-action-publish.js.map +0 -1
- package/dist/cms-seo/cma-client.d.ts +0 -8
- package/dist/cms-seo/cma-client.d.ts.map +0 -1
- package/dist/cms-seo/cma-client.js +0 -11
- package/dist/cms-seo/cma-client.js.map +0 -1
- package/dist/cms-seo/cma-types.d.ts +0 -2
- package/dist/cms-seo/cma-types.d.ts.map +0 -1
- package/dist/cms-seo/cma-types.js +0 -2
- package/dist/cms-seo/cma-types.js.map +0 -1
- package/dist/cms-seo/featured-image-backfill.d.ts +0 -16
- package/dist/cms-seo/featured-image-backfill.d.ts.map +0 -1
- package/dist/cms-seo/featured-image-backfill.js +0 -357
- package/dist/cms-seo/featured-image-backfill.js.map +0 -1
- package/dist/cms-seo/index.d.ts +0 -6
- package/dist/cms-seo/index.d.ts.map +0 -1
- package/dist/cms-seo/index.js +0 -5
- package/dist/cms-seo/index.js.map +0 -1
- package/dist/cms-seo/seo-audit.d.ts +0 -74
- package/dist/cms-seo/seo-audit.d.ts.map +0 -1
- package/dist/cms-seo/seo-audit.js +0 -926
- package/dist/cms-seo/seo-audit.js.map +0 -1
|
@@ -1,926 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
const LOCALE = 'en-US';
|
|
4
|
-
function field(entry, key) {
|
|
5
|
-
return entry.fields?.[key]?.[LOCALE];
|
|
6
|
-
}
|
|
7
|
-
/** Human-readable template identifier — Headwater uses cmsLabel; legacy entries may use slug or name. */
|
|
8
|
-
function templateLabel(template) {
|
|
9
|
-
return (field(template, 'cmsLabel') ??
|
|
10
|
-
field(template, 'slug') ??
|
|
11
|
-
field(template, 'name') ??
|
|
12
|
-
template.sys.id);
|
|
13
|
-
}
|
|
14
|
-
function isGeneralPageTemplate(template) {
|
|
15
|
-
return /general/i.test(templateLabel(template));
|
|
16
|
-
}
|
|
17
|
-
function isArticlePageTemplate(template) {
|
|
18
|
-
return /(publication|news article) template/i.test(templateLabel(template));
|
|
19
|
-
}
|
|
20
|
-
function entryStatus(entry) {
|
|
21
|
-
return entry.sys.publishedAt ? 'published' : 'draft';
|
|
22
|
-
}
|
|
23
|
-
function escapeCsv(value) {
|
|
24
|
-
if (/[",\n]/.test(value))
|
|
25
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
function trimDescription(text, max = 160) {
|
|
29
|
-
const cleaned = text.replace(/\s+/g, ' ').trim();
|
|
30
|
-
if (cleaned.length <= max)
|
|
31
|
-
return cleaned;
|
|
32
|
-
const cut = cleaned.slice(0, max - 1);
|
|
33
|
-
const lastSpace = cut.lastIndexOf(' ');
|
|
34
|
-
return `${(lastSpace > 120 ? cut.slice(0, lastSpace) : cut).trim()}…`;
|
|
35
|
-
}
|
|
36
|
-
async function fetchEntriesByType(client, contentType) {
|
|
37
|
-
const items = [];
|
|
38
|
-
let skip = 0;
|
|
39
|
-
while (true) {
|
|
40
|
-
const page = await client.entry.getMany({
|
|
41
|
-
query: { content_type: contentType, limit: 100, skip },
|
|
42
|
-
});
|
|
43
|
-
items.push(...page.items);
|
|
44
|
-
skip += 100;
|
|
45
|
-
if (skip >= page.total)
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
return items;
|
|
49
|
-
}
|
|
50
|
-
function sameIdSet(a, b) {
|
|
51
|
-
if (a.length !== b.length)
|
|
52
|
-
return false;
|
|
53
|
-
const left = [...a].sort();
|
|
54
|
-
const right = [...b].sort();
|
|
55
|
-
return left.every((id, index) => id === right[index]);
|
|
56
|
-
}
|
|
57
|
-
function linkIds(entry, fieldName) {
|
|
58
|
-
const value = field(entry, fieldName);
|
|
59
|
-
if (!value)
|
|
60
|
-
return [];
|
|
61
|
-
if (Array.isArray(value)) {
|
|
62
|
-
return value
|
|
63
|
-
.map((item) => {
|
|
64
|
-
if (item && typeof item === 'object' && 'sys' in item) {
|
|
65
|
-
const sys = item.sys;
|
|
66
|
-
return sys?.id ?? '';
|
|
67
|
-
}
|
|
68
|
-
return '';
|
|
69
|
-
})
|
|
70
|
-
.filter(Boolean);
|
|
71
|
-
}
|
|
72
|
-
if (typeof value === 'object' && value !== null && 'sys' in value) {
|
|
73
|
-
const id = value.sys?.id;
|
|
74
|
-
return id ? [id] : [];
|
|
75
|
-
}
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
function buildTagMaps(tags, tagTypes) {
|
|
79
|
-
const tagTypeById = new Map();
|
|
80
|
-
for (const tt of tagTypes)
|
|
81
|
-
tagTypeById.set(tt.sys.id, tt);
|
|
82
|
-
const tagById = new Map();
|
|
83
|
-
for (const tag of tags)
|
|
84
|
-
tagById.set(tag.sys.id, tag);
|
|
85
|
-
return { tagById, tagTypeById };
|
|
86
|
-
}
|
|
87
|
-
function primaryTagSlug(article, tagById, tagTypeById, articleTypeById) {
|
|
88
|
-
const tagIds = linkIds(article, 'tags');
|
|
89
|
-
const tags = tagIds.map((id) => tagById.get(id)).filter(Boolean);
|
|
90
|
-
const atId = field(article, 'articleType')?.sys.id;
|
|
91
|
-
const atSlug = atId ? (field(articleTypeById.get(atId), 'slug') ?? '') : '';
|
|
92
|
-
let preferredTagType;
|
|
93
|
-
if (atSlug.includes('resources/news'))
|
|
94
|
-
preferredTagType = 'asset-type';
|
|
95
|
-
else if (atSlug.includes('resources/publications'))
|
|
96
|
-
preferredTagType = 'presentation-type';
|
|
97
|
-
if (preferredTagType) {
|
|
98
|
-
for (const tag of tags) {
|
|
99
|
-
const ttId = field(tag, 'tagType')?.sys.id;
|
|
100
|
-
const tt = ttId ? tagTypeById.get(ttId) : undefined;
|
|
101
|
-
if (tt && field(tt, 'slug') === preferredTagType) {
|
|
102
|
-
const slug = field(tag, 'slug');
|
|
103
|
-
if (slug)
|
|
104
|
-
return slug;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return 'other';
|
|
109
|
-
}
|
|
110
|
-
function articleUrl(config, articleTypeSlug, articleSlug, primaryTag) {
|
|
111
|
-
const base = `${config.articlesBase}/resources/${articleTypeSlug.split('/').pop() ?? articleTypeSlug}`;
|
|
112
|
-
if (config.enablePrimaryTagPartOfSlug) {
|
|
113
|
-
return `${base}/${primaryTag}/${articleSlug}/`;
|
|
114
|
-
}
|
|
115
|
-
return `${base}/${articleSlug}/`;
|
|
116
|
-
}
|
|
117
|
-
function pageUrl(slug) {
|
|
118
|
-
if (slug === 'index')
|
|
119
|
-
return '/';
|
|
120
|
-
return `/${slug}/`;
|
|
121
|
-
}
|
|
122
|
-
function schemaLinkSummary(entry, schemaById) {
|
|
123
|
-
const ids = [...linkIds(entry, 'structuredData'), ...linkIds(entry, 'indexPageStructuredData')];
|
|
124
|
-
if (ids.length === 0)
|
|
125
|
-
return '';
|
|
126
|
-
return ids
|
|
127
|
-
.map((id) => {
|
|
128
|
-
const schema = schemaById.get(id);
|
|
129
|
-
const label = schema ? (field(schema, 'cmsLabel') ?? id) : id;
|
|
130
|
-
return `${label} (${id})`;
|
|
131
|
-
})
|
|
132
|
-
.join('; ');
|
|
133
|
-
}
|
|
134
|
-
function recommendArticleDescription(_config, title, articleTypeSlug, current) {
|
|
135
|
-
if (current.trim().length >= 120 && current.trim().length <= 160)
|
|
136
|
-
return current.trim();
|
|
137
|
-
const isPublication = articleTypeSlug.includes('publications');
|
|
138
|
-
const prefix = isPublication
|
|
139
|
-
? 'Explore this publication on real-world evidence'
|
|
140
|
-
: 'Read the latest news on regulatory-grade real-world evidence';
|
|
141
|
-
const base = `${prefix}: ${title}.`;
|
|
142
|
-
return trimDescription(base);
|
|
143
|
-
}
|
|
144
|
-
function classifyRowSchema(contentType, slug, url, _config, articleTypeSlug) {
|
|
145
|
-
if (contentType === 'tagType') {
|
|
146
|
-
return {
|
|
147
|
-
types: 'N/A',
|
|
148
|
-
action: 'none',
|
|
149
|
-
notes: 'Taxonomy only — no public URL or CMS SEO fields',
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
if (url.includes('/resources/') && url.split('/').filter(Boolean).length === 3) {
|
|
153
|
-
return {
|
|
154
|
-
types: 'CollectionPage',
|
|
155
|
-
action: 'link template',
|
|
156
|
-
notes: 'Combo URL — description auto-generated at runtime; link CollectionPage schema via tag or shared template',
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
if (contentType === 'template') {
|
|
160
|
-
return {
|
|
161
|
-
types: 'Organization; WebSite',
|
|
162
|
-
action: 'link',
|
|
163
|
-
notes: 'Template-level Organization + WebSite (SearchAction deferred until search index populated on Headwater)',
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
if (contentType === 'page') {
|
|
167
|
-
if (slug === 'about' || slug === 'about-us') {
|
|
168
|
-
return {
|
|
169
|
-
types: 'AboutPage',
|
|
170
|
-
action: 'link',
|
|
171
|
-
notes: 'Entry or shared AboutPage schema; inherits Organization/WebSite from template',
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (slug === 'contact' || slug === 'contact-us') {
|
|
175
|
-
return { types: 'ContactPage', action: 'link', notes: 'Entry-level ContactPage schema' };
|
|
176
|
-
}
|
|
177
|
-
if (slug === 'index' || slug === 'home') {
|
|
178
|
-
return {
|
|
179
|
-
types: 'WebPage',
|
|
180
|
-
action: 'link',
|
|
181
|
-
notes: 'Homepage WebPage + template Organization/WebSite',
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
if (slug.includes('privacy') || slug.includes('cookie')) {
|
|
185
|
-
return { types: 'WebPage', action: 'keep', notes: 'Legal page — minimal rich schema' };
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
types: 'WebPage',
|
|
189
|
-
action: 'link',
|
|
190
|
-
notes: 'Shared WebPage Mustache template (page.title, page.description, breadcrumbs)',
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
if (contentType === 'articleType') {
|
|
194
|
-
return {
|
|
195
|
-
types: 'CollectionPage',
|
|
196
|
-
action: 'link',
|
|
197
|
-
notes: 'indexPageStructuredData — CollectionPage for resource hub',
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
if (contentType === 'customType') {
|
|
201
|
-
return {
|
|
202
|
-
types: 'CollectionPage',
|
|
203
|
-
action: 'link',
|
|
204
|
-
notes: 'indexPageStructuredData on customType (people/categories index)',
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (contentType === 'tag') {
|
|
208
|
-
return {
|
|
209
|
-
types: 'CollectionPage',
|
|
210
|
-
action: 'link',
|
|
211
|
-
notes: 'tag.structuredData — CollectionPage for category hub',
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
if (contentType === 'article') {
|
|
215
|
-
if (articleTypeSlug?.includes('publications')) {
|
|
216
|
-
return {
|
|
217
|
-
types: 'ScholarlyArticle',
|
|
218
|
-
action: 'link via articleType',
|
|
219
|
-
notes: 'Prefer articleType.structuredData template; entry override only if unique',
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
types: 'BlogPosting',
|
|
224
|
-
action: 'link via articleType',
|
|
225
|
-
notes: 'articleType.structuredData BlogPosting template with article.* Mustache vars',
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
if (contentType === 'person') {
|
|
229
|
-
return {
|
|
230
|
-
types: 'ProfilePage; Person',
|
|
231
|
-
action: 'link',
|
|
232
|
-
notes: 'person.structuredData — ProfilePage + nested Person',
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
return { types: 'WebPage', action: 'keep', notes: '' };
|
|
236
|
-
}
|
|
237
|
-
export async function runSeoAudit(client, config, options) {
|
|
238
|
-
const [pages, articles, articleTypes, tags, tagTypes, customTypes, people, schemas, templates] = await Promise.all([
|
|
239
|
-
fetchEntriesByType(client, 'page'),
|
|
240
|
-
fetchEntriesByType(client, 'article'),
|
|
241
|
-
fetchEntriesByType(client, 'articleType'),
|
|
242
|
-
fetchEntriesByType(client, 'tag'),
|
|
243
|
-
fetchEntriesByType(client, 'tagType'),
|
|
244
|
-
fetchEntriesByType(client, 'customType'),
|
|
245
|
-
fetchEntriesByType(client, 'person'),
|
|
246
|
-
fetchEntriesByType(client, 'schema'),
|
|
247
|
-
fetchEntriesByType(client, 'template'),
|
|
248
|
-
]);
|
|
249
|
-
const schemaById = new Map(schemas.map((s) => [s.sys.id, s]));
|
|
250
|
-
const articleTypeById = new Map(articleTypes.map((at) => [at.sys.id, at]));
|
|
251
|
-
const { tagById, tagTypeById } = buildTagMaps(tags, tagTypes);
|
|
252
|
-
const rows = [];
|
|
253
|
-
const usedDescriptions = new Set();
|
|
254
|
-
function pushRow(partial) {
|
|
255
|
-
const recommended = partial.recommendedDescription.trim();
|
|
256
|
-
const flags = partial.flags ? partial.flags.split('; ').filter(Boolean) : [];
|
|
257
|
-
if (recommended && usedDescriptions.has(recommended.toLowerCase()))
|
|
258
|
-
flags.push('duplicate recommended description');
|
|
259
|
-
if (recommended)
|
|
260
|
-
usedDescriptions.add(recommended.toLowerCase());
|
|
261
|
-
if (partial.chars > 0 && (partial.chars < 120 || partial.chars > 160))
|
|
262
|
-
flags.push('char count outside 120–160');
|
|
263
|
-
if (partial.flags.includes('auto-generated'))
|
|
264
|
-
partial.reviewStatus = partial.reviewStatus ?? 'Skip';
|
|
265
|
-
else if (flags.some((f) => f.includes('TBC')))
|
|
266
|
-
partial.reviewStatus = partial.reviewStatus ?? 'Revise';
|
|
267
|
-
else
|
|
268
|
-
partial.reviewStatus = partial.reviewStatus ?? 'Approved';
|
|
269
|
-
rows.push({
|
|
270
|
-
...partial,
|
|
271
|
-
flags: flags.join('; '),
|
|
272
|
-
reviewStatus: partial.reviewStatus ?? 'Approved',
|
|
273
|
-
applied: partial.applied ?? '',
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
// Pages
|
|
277
|
-
for (const page of pages) {
|
|
278
|
-
const slug = field(page, 'slug') ?? page.sys.id;
|
|
279
|
-
const url = pageUrl(slug);
|
|
280
|
-
const current = field(page, 'description') ?? '';
|
|
281
|
-
const recommended = config.pageDescriptions[slug] ??
|
|
282
|
-
(current.trim() ? trimDescription(current) : trimDescription(config.siteDescription));
|
|
283
|
-
const schema = classifyRowSchema('page', slug, url, config);
|
|
284
|
-
pushRow({
|
|
285
|
-
url,
|
|
286
|
-
contentType: 'page',
|
|
287
|
-
slug,
|
|
288
|
-
entryId: page.sys.id,
|
|
289
|
-
status: entryStatus(page),
|
|
290
|
-
currentDescription: current,
|
|
291
|
-
chars: current.length,
|
|
292
|
-
recommendedDescription: recommended,
|
|
293
|
-
targetKeyword: config.pageKeywords[slug] ?? slug.replace(/-/g, ' '),
|
|
294
|
-
currentSchemaLinks: schemaLinkSummary(page, schemaById),
|
|
295
|
-
recommendedSchemaTypes: schema.types,
|
|
296
|
-
schemaAction: schema.action,
|
|
297
|
-
schemaTemplateNotes: schema.notes,
|
|
298
|
-
flags: slug === 'index' && url === '/' ? 'root URL alias' : '',
|
|
299
|
-
applied: '',
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
// Article types
|
|
303
|
-
for (const at of articleTypes) {
|
|
304
|
-
const slug = field(at, 'slug') ?? at.sys.id;
|
|
305
|
-
const segment = slug.split('/').pop() ?? slug;
|
|
306
|
-
const url = `/resources/${segment}/`;
|
|
307
|
-
const current = field(at, 'indexPageDescription') ?? '';
|
|
308
|
-
const recommended = config.indexDescriptions[slug] ??
|
|
309
|
-
(current.trim() ||
|
|
310
|
-
trimDescription(`Browse ${config.siteName} ${segment.replace(/-/g, ' ')} — regulatory-grade real-world evidence and insights.`));
|
|
311
|
-
const schema = classifyRowSchema('articleType', slug, url, config);
|
|
312
|
-
pushRow({
|
|
313
|
-
url,
|
|
314
|
-
contentType: 'articleType',
|
|
315
|
-
slug,
|
|
316
|
-
entryId: at.sys.id,
|
|
317
|
-
status: entryStatus(at),
|
|
318
|
-
currentDescription: current,
|
|
319
|
-
chars: current.length,
|
|
320
|
-
recommendedDescription: recommended,
|
|
321
|
-
targetKeyword: config.indexKeywords[slug] ?? segment,
|
|
322
|
-
currentSchemaLinks: schemaLinkSummary(at, schemaById),
|
|
323
|
-
recommendedSchemaTypes: schema.types,
|
|
324
|
-
schemaAction: schema.action,
|
|
325
|
-
schemaTemplateNotes: schema.notes,
|
|
326
|
-
flags: '',
|
|
327
|
-
applied: '',
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
// Custom types
|
|
331
|
-
for (const ct of customTypes) {
|
|
332
|
-
const slug = field(ct, 'slug') ?? ct.sys.id;
|
|
333
|
-
const url = `/${slug}/`;
|
|
334
|
-
const current = field(ct, 'indexPageDescription') ?? '';
|
|
335
|
-
const recommended = config.indexDescriptions[slug] ??
|
|
336
|
-
(current.trim() ||
|
|
337
|
-
trimDescription(`Explore ${slug.replace(/-/g, ' ')} at ${config.siteName}.`));
|
|
338
|
-
const schema = classifyRowSchema('customType', slug, url, config);
|
|
339
|
-
pushRow({
|
|
340
|
-
url,
|
|
341
|
-
contentType: 'customType',
|
|
342
|
-
slug,
|
|
343
|
-
entryId: ct.sys.id,
|
|
344
|
-
status: entryStatus(ct),
|
|
345
|
-
currentDescription: current,
|
|
346
|
-
chars: current.length,
|
|
347
|
-
recommendedDescription: recommended,
|
|
348
|
-
targetKeyword: config.indexKeywords[slug] ?? slug,
|
|
349
|
-
currentSchemaLinks: schemaLinkSummary(ct, schemaById),
|
|
350
|
-
recommendedSchemaTypes: schema.types,
|
|
351
|
-
schemaAction: schema.action,
|
|
352
|
-
schemaTemplateNotes: schema.notes,
|
|
353
|
-
flags: '',
|
|
354
|
-
applied: '',
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
// Tags + combo URLs
|
|
358
|
-
for (const tag of tags) {
|
|
359
|
-
const slug = field(tag, 'slug') ?? tag.sys.id;
|
|
360
|
-
const url = `${config.tagsBase}/${slug}/`;
|
|
361
|
-
const current = field(tag, 'description') ?? '';
|
|
362
|
-
const recommended = current.trim() ||
|
|
363
|
-
trimDescription(`Resources and insights tagged ${slug.replace(/-/g, ' ')} from ${config.siteName}.`);
|
|
364
|
-
const schema = classifyRowSchema('tag', slug, url, config);
|
|
365
|
-
pushRow({
|
|
366
|
-
url,
|
|
367
|
-
contentType: 'tag',
|
|
368
|
-
slug,
|
|
369
|
-
entryId: tag.sys.id,
|
|
370
|
-
status: entryStatus(tag),
|
|
371
|
-
currentDescription: current,
|
|
372
|
-
chars: current.length,
|
|
373
|
-
recommendedDescription: recommended,
|
|
374
|
-
targetKeyword: slug.replace(/-/g, ' '),
|
|
375
|
-
currentSchemaLinks: schemaLinkSummary(tag, schemaById),
|
|
376
|
-
recommendedSchemaTypes: schema.types,
|
|
377
|
-
schemaAction: schema.action,
|
|
378
|
-
schemaTemplateNotes: schema.notes,
|
|
379
|
-
flags: '',
|
|
380
|
-
applied: '',
|
|
381
|
-
});
|
|
382
|
-
for (const at of articleTypes) {
|
|
383
|
-
const atSlug = field(at, 'slug') ?? '';
|
|
384
|
-
const segment = atSlug.split('/').pop() ?? atSlug;
|
|
385
|
-
const comboUrl = `/resources/${segment}/${slug}/`;
|
|
386
|
-
const autoDesc = `Browse the latest ${segment.replace(/-/g, ' ')} resources related to ${slug.replace(/-/g, ' ')}.`;
|
|
387
|
-
const comboSchema = classifyRowSchema('tag', slug, comboUrl, config);
|
|
388
|
-
pushRow({
|
|
389
|
-
url: comboUrl,
|
|
390
|
-
contentType: 'articleType+tag',
|
|
391
|
-
slug: `${atSlug}+${slug}`,
|
|
392
|
-
entryId: `${at.sys.id}+${tag.sys.id}`,
|
|
393
|
-
status: entryStatus(tag),
|
|
394
|
-
currentDescription: autoDesc,
|
|
395
|
-
chars: autoDesc.length,
|
|
396
|
-
recommendedDescription: autoDesc,
|
|
397
|
-
targetKeyword: `${segment} ${slug}`,
|
|
398
|
-
currentSchemaLinks: schemaLinkSummary(tag, schemaById),
|
|
399
|
-
recommendedSchemaTypes: comboSchema.types,
|
|
400
|
-
schemaAction: 'document only',
|
|
401
|
-
schemaTemplateNotes: comboSchema.notes,
|
|
402
|
-
flags: 'auto-generated description — not CMS-editable',
|
|
403
|
-
reviewStatus: 'Skip',
|
|
404
|
-
applied: '',
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
// tagType taxonomy
|
|
409
|
-
for (const tt of tagTypes) {
|
|
410
|
-
const slug = field(tt, 'slug') ?? tt.sys.id;
|
|
411
|
-
const schema = classifyRowSchema('tagType', slug, '', config);
|
|
412
|
-
pushRow({
|
|
413
|
-
url: 'N/A',
|
|
414
|
-
contentType: 'tagType',
|
|
415
|
-
slug,
|
|
416
|
-
entryId: tt.sys.id,
|
|
417
|
-
status: entryStatus(tt),
|
|
418
|
-
currentDescription: '',
|
|
419
|
-
chars: 0,
|
|
420
|
-
recommendedDescription: '',
|
|
421
|
-
targetKeyword: '',
|
|
422
|
-
currentSchemaLinks: '',
|
|
423
|
-
recommendedSchemaTypes: schema.types,
|
|
424
|
-
schemaAction: schema.action,
|
|
425
|
-
schemaTemplateNotes: schema.notes,
|
|
426
|
-
flags: 'taxonomy only',
|
|
427
|
-
reviewStatus: 'Skip',
|
|
428
|
-
applied: '',
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
// People
|
|
432
|
-
for (const person of people) {
|
|
433
|
-
const slug = field(person, 'slug') ?? person.sys.id;
|
|
434
|
-
const url = `${config.peopleBase}/${slug}/`;
|
|
435
|
-
const name = field(person, 'name') ?? slug;
|
|
436
|
-
const jobTitle = field(person, 'jobTitle') ?? '';
|
|
437
|
-
const current = field(person, 'description') ?? '';
|
|
438
|
-
const recommended = current.trim() ||
|
|
439
|
-
trimDescription(jobTitle
|
|
440
|
-
? `${name}, ${jobTitle} at ${config.orgName}. Learn about their work in real-world evidence.`
|
|
441
|
-
: `${name} at ${config.orgName}. Learn about their role and expertise.`);
|
|
442
|
-
const schema = classifyRowSchema('person', slug, url, config);
|
|
443
|
-
pushRow({
|
|
444
|
-
url,
|
|
445
|
-
contentType: 'person',
|
|
446
|
-
slug,
|
|
447
|
-
entryId: person.sys.id,
|
|
448
|
-
status: entryStatus(person),
|
|
449
|
-
currentDescription: current,
|
|
450
|
-
chars: current.length,
|
|
451
|
-
recommendedDescription: recommended,
|
|
452
|
-
targetKeyword: name,
|
|
453
|
-
currentSchemaLinks: schemaLinkSummary(person, schemaById),
|
|
454
|
-
recommendedSchemaTypes: schema.types,
|
|
455
|
-
schemaAction: schema.action,
|
|
456
|
-
schemaTemplateNotes: schema.notes,
|
|
457
|
-
flags: '',
|
|
458
|
-
applied: '',
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
// Articles (optionally sample)
|
|
462
|
-
const articlesByType = new Map();
|
|
463
|
-
for (const article of articles) {
|
|
464
|
-
const atId = field(article, 'articleType')?.sys.id;
|
|
465
|
-
const atSlug = atId
|
|
466
|
-
? (field(articleTypeById.get(atId), 'slug') ?? 'unknown')
|
|
467
|
-
: 'unknown';
|
|
468
|
-
if (!articlesByType.has(atSlug))
|
|
469
|
-
articlesByType.set(atSlug, []);
|
|
470
|
-
articlesByType.get(atSlug).push(article);
|
|
471
|
-
}
|
|
472
|
-
for (const [atSlug, typeArticles] of articlesByType) {
|
|
473
|
-
const sorted = [...typeArticles].sort((a, b) => {
|
|
474
|
-
const da = field(a, 'date') ?? '';
|
|
475
|
-
const db = field(b, 'date') ?? '';
|
|
476
|
-
return db.localeCompare(da);
|
|
477
|
-
});
|
|
478
|
-
const selected = options.sampleArticlesOnly ? sorted.slice(0, 5) : sorted;
|
|
479
|
-
for (const article of selected) {
|
|
480
|
-
const slug = field(article, 'slug') ?? article.sys.id;
|
|
481
|
-
const title = field(article, 'title') ?? slug;
|
|
482
|
-
const primaryTag = primaryTagSlug(article, tagById, tagTypeById, articleTypeById);
|
|
483
|
-
const url = articleUrl(config, atSlug, slug, primaryTag);
|
|
484
|
-
const current = field(article, 'description') ?? '';
|
|
485
|
-
const recommended = recommendArticleDescription(config, title, atSlug, current);
|
|
486
|
-
const schema = classifyRowSchema('article', slug, url, config, atSlug);
|
|
487
|
-
pushRow({
|
|
488
|
-
url,
|
|
489
|
-
contentType: 'article',
|
|
490
|
-
slug,
|
|
491
|
-
entryId: article.sys.id,
|
|
492
|
-
status: entryStatus(article),
|
|
493
|
-
currentDescription: current,
|
|
494
|
-
chars: current.length,
|
|
495
|
-
recommendedDescription: recommended,
|
|
496
|
-
targetKeyword: title.slice(0, 40),
|
|
497
|
-
currentSchemaLinks: schemaLinkSummary(article, schemaById),
|
|
498
|
-
recommendedSchemaTypes: schema.types,
|
|
499
|
-
schemaAction: schema.action,
|
|
500
|
-
schemaTemplateNotes: schema.notes,
|
|
501
|
-
flags: options.sampleArticlesOnly ? 'sample row' : '',
|
|
502
|
-
applied: '',
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
// Templates (sitewide schema)
|
|
507
|
-
for (const template of templates) {
|
|
508
|
-
const slug = templateLabel(template);
|
|
509
|
-
const generalTemplate = isGeneralPageTemplate(template);
|
|
510
|
-
const articleTemplate = isArticlePageTemplate(template);
|
|
511
|
-
const linkOrgWebsite = generalTemplate || articleTemplate;
|
|
512
|
-
pushRow({
|
|
513
|
-
url: 'sitewide',
|
|
514
|
-
contentType: 'template',
|
|
515
|
-
slug,
|
|
516
|
-
entryId: template.sys.id,
|
|
517
|
-
status: entryStatus(template),
|
|
518
|
-
currentDescription: '',
|
|
519
|
-
chars: 0,
|
|
520
|
-
recommendedDescription: '',
|
|
521
|
-
targetKeyword: '',
|
|
522
|
-
currentSchemaLinks: schemaLinkSummary(template, schemaById),
|
|
523
|
-
recommendedSchemaTypes: linkOrgWebsite ? 'Organization; WebSite' : '',
|
|
524
|
-
schemaAction: linkOrgWebsite ? 'link Organization + WebSite schema entries' : 'skip',
|
|
525
|
-
schemaTemplateNotes: generalTemplate
|
|
526
|
-
? 'General template structuredData — shared across pages using this template'
|
|
527
|
-
: articleTemplate
|
|
528
|
-
? 'Article template structuredData — Organization/WebSite plus articleType article schema'
|
|
529
|
-
: 'Other templates — no sitewide schema',
|
|
530
|
-
flags: linkOrgWebsite ? '' : 'non-sitewide template',
|
|
531
|
-
reviewStatus: linkOrgWebsite ? 'Approved' : 'Skip',
|
|
532
|
-
applied: '',
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
const csvHeader = [
|
|
536
|
-
'URL',
|
|
537
|
-
'Content type',
|
|
538
|
-
'Slug',
|
|
539
|
-
'Entry ID',
|
|
540
|
-
'Status',
|
|
541
|
-
'Current description',
|
|
542
|
-
'Chars',
|
|
543
|
-
'Recommended description',
|
|
544
|
-
'Target keyword',
|
|
545
|
-
'Current schema links',
|
|
546
|
-
'Recommended schema type(s)',
|
|
547
|
-
'Schema action',
|
|
548
|
-
'Schema template notes',
|
|
549
|
-
'Flags',
|
|
550
|
-
'Review status',
|
|
551
|
-
'Applied?',
|
|
552
|
-
];
|
|
553
|
-
const csvLines = [
|
|
554
|
-
csvHeader.join(','),
|
|
555
|
-
...rows.map((r) => [
|
|
556
|
-
r.url,
|
|
557
|
-
r.contentType,
|
|
558
|
-
r.slug,
|
|
559
|
-
r.entryId,
|
|
560
|
-
r.status,
|
|
561
|
-
r.currentDescription,
|
|
562
|
-
String(r.chars),
|
|
563
|
-
r.recommendedDescription,
|
|
564
|
-
r.targetKeyword,
|
|
565
|
-
r.currentSchemaLinks,
|
|
566
|
-
r.recommendedSchemaTypes,
|
|
567
|
-
r.schemaAction,
|
|
568
|
-
r.schemaTemplateNotes,
|
|
569
|
-
r.flags,
|
|
570
|
-
r.reviewStatus,
|
|
571
|
-
r.applied,
|
|
572
|
-
]
|
|
573
|
-
.map(escapeCsv)
|
|
574
|
-
.join(',')),
|
|
575
|
-
];
|
|
576
|
-
await mkdir(path.dirname(config.outputCsvPath), { recursive: true });
|
|
577
|
-
await writeFile(config.outputCsvPath, `${csvLines.join('\n')}\n`, 'utf8');
|
|
578
|
-
if (options.apply) {
|
|
579
|
-
await applySeoAudit(client, config, rows, {
|
|
580
|
-
schemas,
|
|
581
|
-
templates,
|
|
582
|
-
articleTypes,
|
|
583
|
-
schemaById,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
return { rows, csvPath: config.outputCsvPath };
|
|
587
|
-
}
|
|
588
|
-
function findSchemaIdByLabel(schemaById, cmsLabel) {
|
|
589
|
-
for (const [id, entry] of schemaById) {
|
|
590
|
-
if (field(entry, 'cmsLabel') === cmsLabel)
|
|
591
|
-
return id;
|
|
592
|
-
}
|
|
593
|
-
return undefined;
|
|
594
|
-
}
|
|
595
|
-
async function ensureSchemaEntry(client, existingId, cmsLabel, markup, schemaById) {
|
|
596
|
-
const resolvedId = existingId ?? findSchemaIdByLabel(schemaById, cmsLabel);
|
|
597
|
-
if (resolvedId && schemaById.has(resolvedId)) {
|
|
598
|
-
const existing = schemaById.get(resolvedId);
|
|
599
|
-
if (field(existing, 'markup') !== markup) {
|
|
600
|
-
const updated = await client.entry.update({ entryId: resolvedId }, {
|
|
601
|
-
sys: { version: existing.sys.version },
|
|
602
|
-
fields: {
|
|
603
|
-
...existing.fields,
|
|
604
|
-
markup: { [LOCALE]: markup },
|
|
605
|
-
},
|
|
606
|
-
});
|
|
607
|
-
schemaById.set(resolvedId, updated);
|
|
608
|
-
}
|
|
609
|
-
return resolvedId;
|
|
610
|
-
}
|
|
611
|
-
const created = await client.entry.create({ contentTypeId: 'schema' }, { fields: { cmsLabel: { [LOCALE]: cmsLabel }, markup: { [LOCALE]: markup } } });
|
|
612
|
-
schemaById.set(created.sys.id, created);
|
|
613
|
-
return created.sys.id;
|
|
614
|
-
}
|
|
615
|
-
function organizationMarkup(config) {
|
|
616
|
-
const sameAs = config.linkedInUrl
|
|
617
|
-
? `,\n "sameAs": [\n "${config.linkedInUrl}"\n ]`
|
|
618
|
-
: '';
|
|
619
|
-
return `{
|
|
620
|
-
"@context": "https://schema.org",
|
|
621
|
-
"@graph": [
|
|
622
|
-
{
|
|
623
|
-
"@type": ["Organization", "MedicalOrganization"],
|
|
624
|
-
"@id": "{{{baseUrl}}}/#organization",
|
|
625
|
-
"name": "${config.orgName}",
|
|
626
|
-
"url": "{{{baseUrl}}}",
|
|
627
|
-
"description": "${config.siteDescription.replace(/"/g, '\\"')}"${sameAs}
|
|
628
|
-
}
|
|
629
|
-
]
|
|
630
|
-
}`;
|
|
631
|
-
}
|
|
632
|
-
function websiteMarkup(config, includeSearch) {
|
|
633
|
-
const searchBlock = includeSearch
|
|
634
|
-
? `,
|
|
635
|
-
"potentialAction": {
|
|
636
|
-
"@type": "SearchAction",
|
|
637
|
-
"target": {
|
|
638
|
-
"@type": "EntryPoint",
|
|
639
|
-
"urlTemplate": "{{{baseUrl}}}/search?q={search_term_string}"
|
|
640
|
-
},
|
|
641
|
-
"query-input": "required name=search_term_string"
|
|
642
|
-
}`
|
|
643
|
-
: '';
|
|
644
|
-
return `{
|
|
645
|
-
"@context": "https://schema.org",
|
|
646
|
-
"@graph": [
|
|
647
|
-
{
|
|
648
|
-
"@type": "WebSite",
|
|
649
|
-
"@id": "{{{baseUrl}}}/#website",
|
|
650
|
-
"url": "{{{baseUrl}}}",
|
|
651
|
-
"name": "${config.orgName}",
|
|
652
|
-
"description": "${config.siteDescription.replace(/"/g, '\\"')}",
|
|
653
|
-
"publisher": {
|
|
654
|
-
"@id": "{{{baseUrl}}}/#organization"
|
|
655
|
-
}${searchBlock}
|
|
656
|
-
}
|
|
657
|
-
]
|
|
658
|
-
}`;
|
|
659
|
-
}
|
|
660
|
-
function webPageMarkup(pageType = 'WebPage') {
|
|
661
|
-
return `{
|
|
662
|
-
"@context": "https://schema.org",
|
|
663
|
-
"@graph": [
|
|
664
|
-
{
|
|
665
|
-
"@type": "${pageType}",
|
|
666
|
-
"@id": "{{{currentUrl}}}",
|
|
667
|
-
"url": "{{{currentUrl}}}",
|
|
668
|
-
"name": "{{page.title}}",
|
|
669
|
-
"description": "{{page.description}}",
|
|
670
|
-
"image": "{{{page.imageUrl}}}",
|
|
671
|
-
"publisher": {
|
|
672
|
-
"@id": "{{{baseUrl}}}/#organization"
|
|
673
|
-
},
|
|
674
|
-
"isPartOf": {
|
|
675
|
-
"@id": "{{{baseUrl}}}/#website"
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
]
|
|
679
|
-
}`;
|
|
680
|
-
}
|
|
681
|
-
function collectionPageMarkup() {
|
|
682
|
-
return `{
|
|
683
|
-
"@context": "https://schema.org",
|
|
684
|
-
"@graph": [
|
|
685
|
-
{
|
|
686
|
-
"@type": "CollectionPage",
|
|
687
|
-
"@id": "{{{currentUrl}}}",
|
|
688
|
-
"url": "{{{currentUrl}}}",
|
|
689
|
-
"name": "{{page.title}}",
|
|
690
|
-
"description": "{{page.description}}",
|
|
691
|
-
"publisher": {
|
|
692
|
-
"@id": "{{{baseUrl}}}/#organization"
|
|
693
|
-
},
|
|
694
|
-
"isPartOf": {
|
|
695
|
-
"@id": "{{{baseUrl}}}/#website"
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
]
|
|
699
|
-
}`;
|
|
700
|
-
}
|
|
701
|
-
function blogPostingMarkup(config) {
|
|
702
|
-
return `{
|
|
703
|
-
"@context": "https://schema.org",
|
|
704
|
-
"@graph": [
|
|
705
|
-
{
|
|
706
|
-
"@type": "BlogPosting",
|
|
707
|
-
"@id": "{{{currentUrl}}}",
|
|
708
|
-
"url": "{{{currentUrl}}}",
|
|
709
|
-
"headline": "{{article.title}}",
|
|
710
|
-
"description": "{{article.description}}",
|
|
711
|
-
"datePublished": "{{article.date}}",
|
|
712
|
-
"image": "{{{article.imageUrl}}}",
|
|
713
|
-
"author": {
|
|
714
|
-
"@type": "Organization",
|
|
715
|
-
"name": "${config.orgName}"
|
|
716
|
-
},
|
|
717
|
-
"publisher": {
|
|
718
|
-
"@id": "{{{baseUrl}}}/#organization"
|
|
719
|
-
},
|
|
720
|
-
"isPartOf": {
|
|
721
|
-
"@id": "{{{baseUrl}}}/#website"
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
]
|
|
725
|
-
}`;
|
|
726
|
-
}
|
|
727
|
-
function scholarlyArticleMarkup(config) {
|
|
728
|
-
return `{
|
|
729
|
-
"@context": "https://schema.org",
|
|
730
|
-
"@graph": [
|
|
731
|
-
{
|
|
732
|
-
"@type": "ScholarlyArticle",
|
|
733
|
-
"@id": "{{{currentUrl}}}",
|
|
734
|
-
"url": "{{{currentUrl}}}",
|
|
735
|
-
"headline": "{{article.title}}",
|
|
736
|
-
"description": "{{article.description}}",
|
|
737
|
-
"datePublished": "{{article.date}}",
|
|
738
|
-
"image": "{{{article.imageUrl}}}",
|
|
739
|
-
"author": {
|
|
740
|
-
"@type": "Organization",
|
|
741
|
-
"name": "${config.orgName}"
|
|
742
|
-
},
|
|
743
|
-
"publisher": {
|
|
744
|
-
"@id": "{{{baseUrl}}}/#organization"
|
|
745
|
-
},
|
|
746
|
-
"isPartOf": {
|
|
747
|
-
"@id": "{{{baseUrl}}}/#website"
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
]
|
|
751
|
-
}`;
|
|
752
|
-
}
|
|
753
|
-
export function profilePageMarkup(config) {
|
|
754
|
-
return `{
|
|
755
|
-
"@context": "https://schema.org",
|
|
756
|
-
"@graph": [
|
|
757
|
-
{
|
|
758
|
-
"@type": "ProfilePage",
|
|
759
|
-
"@id": "{{{currentUrl}}}",
|
|
760
|
-
"url": "{{{currentUrl}}}",
|
|
761
|
-
"name": "{{person.name}}",
|
|
762
|
-
"description": "{{page.description}}",
|
|
763
|
-
"mainEntity": {
|
|
764
|
-
"@type": "Person",
|
|
765
|
-
"@id": "{{{currentUrl}}}#person",
|
|
766
|
-
"name": "{{person.name}}",
|
|
767
|
-
"jobTitle": "{{person.jobTitle}}",
|
|
768
|
-
"image": "{{{person.imageUrl}}}",
|
|
769
|
-
"worksFor": {
|
|
770
|
-
"@id": "{{{baseUrl}}}/#organization"
|
|
771
|
-
}
|
|
772
|
-
},
|
|
773
|
-
"publisher": {
|
|
774
|
-
"@id": "{{{baseUrl}}}/#organization"
|
|
775
|
-
},
|
|
776
|
-
"isPartOf": {
|
|
777
|
-
"@id": "{{{baseUrl}}}/#website"
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
]
|
|
781
|
-
}`;
|
|
782
|
-
}
|
|
783
|
-
async function updateEntryField(client, entryId, fields) {
|
|
784
|
-
const entry = await client.entry.get({ entryId });
|
|
785
|
-
const merged = { ...entry.fields };
|
|
786
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
787
|
-
merged[key] = { [LOCALE]: value };
|
|
788
|
-
}
|
|
789
|
-
await client.entry.update({ entryId }, { ...entry, fields: merged });
|
|
790
|
-
}
|
|
791
|
-
async function linkSchemaArray(client, entryId, fieldName, schemaIds) {
|
|
792
|
-
const entry = await client.entry.get({ entryId });
|
|
793
|
-
const links = schemaIds.map((id) => ({
|
|
794
|
-
sys: { type: 'Link', linkType: 'Entry', id },
|
|
795
|
-
}));
|
|
796
|
-
const merged = {
|
|
797
|
-
...entry.fields,
|
|
798
|
-
[fieldName]: { [LOCALE]: links },
|
|
799
|
-
};
|
|
800
|
-
await client.entry.update({ entryId }, { ...entry, fields: merged });
|
|
801
|
-
}
|
|
802
|
-
async function applySeoAudit(client, config, rows, ctx) {
|
|
803
|
-
const includeSearch = config.enableSearchAction ?? false;
|
|
804
|
-
const orgId = await ensureSchemaEntry(client, config.schemaIds?.organization, `${config.orgName} — Organization`, organizationMarkup(config), ctx.schemaById);
|
|
805
|
-
const websiteId = await ensureSchemaEntry(client, config.schemaIds?.website, `${config.orgName} — WebSite`, websiteMarkup(config, includeSearch), ctx.schemaById);
|
|
806
|
-
if (config.schemaIds?.website && includeSearch && ctx.schemaById.has(config.schemaIds.website)) {
|
|
807
|
-
const entry = await client.entry.get({ entryId: config.schemaIds.website });
|
|
808
|
-
const merged = {
|
|
809
|
-
...entry.fields,
|
|
810
|
-
markup: { [LOCALE]: websiteMarkup(config, true) },
|
|
811
|
-
};
|
|
812
|
-
await client.entry.update({ entryId: config.schemaIds.website }, { ...entry, fields: merged });
|
|
813
|
-
}
|
|
814
|
-
const webPageId = await ensureSchemaEntry(client, config.schemaIds?.webPage, `${config.orgName} — WebPage`, webPageMarkup('WebPage'), ctx.schemaById);
|
|
815
|
-
const collectionId = await ensureSchemaEntry(client, config.schemaIds?.collectionPage, `${config.orgName} — CollectionPage`, collectionPageMarkup(), ctx.schemaById);
|
|
816
|
-
const blogId = await ensureSchemaEntry(client, config.schemaIds?.blogPosting, `${config.orgName} — BlogPosting`, blogPostingMarkup(config), ctx.schemaById);
|
|
817
|
-
const scholarlyId = await ensureSchemaEntry(client, config.schemaIds?.scholarlyArticle, `${config.orgName} — ScholarlyArticle`, scholarlyArticleMarkup(config), ctx.schemaById);
|
|
818
|
-
const profilePageId = await ensureSchemaEntry(client, config.schemaIds?.profilePage, `${config.orgName} — ProfilePage (Person)`, profilePageMarkup(config), ctx.schemaById);
|
|
819
|
-
// Template-level Organization + WebSite (general pages + article templates)
|
|
820
|
-
for (const template of ctx.templates) {
|
|
821
|
-
if (!isGeneralPageTemplate(template) && !isArticlePageTemplate(template))
|
|
822
|
-
continue;
|
|
823
|
-
await linkSchemaArray(client, template.sys.id, 'structuredData', [orgId, websiteId]);
|
|
824
|
-
}
|
|
825
|
-
// Article type schema templates
|
|
826
|
-
for (const at of ctx.articleTypes) {
|
|
827
|
-
const slug = field(at, 'slug') ?? '';
|
|
828
|
-
const ids = slug.includes('publications')
|
|
829
|
-
? [collectionId, scholarlyId]
|
|
830
|
-
: [collectionId, blogId];
|
|
831
|
-
await linkSchemaArray(client, at.sys.id, 'indexPageStructuredData', [collectionId]);
|
|
832
|
-
await linkSchemaArray(client, at.sys.id, 'structuredData', [ids[1]]);
|
|
833
|
-
}
|
|
834
|
-
for (const row of rows) {
|
|
835
|
-
if (row.reviewStatus !== 'Approved')
|
|
836
|
-
continue;
|
|
837
|
-
if (row.flags.includes('auto-generated') || row.flags.includes('taxonomy only'))
|
|
838
|
-
continue;
|
|
839
|
-
if (!row.recommendedDescription || row.recommendedDescription === row.currentDescription) {
|
|
840
|
-
// still may need schema links
|
|
841
|
-
}
|
|
842
|
-
try {
|
|
843
|
-
if (row.contentType === 'page' &&
|
|
844
|
-
row.recommendedDescription &&
|
|
845
|
-
row.recommendedDescription !== row.currentDescription) {
|
|
846
|
-
await updateEntryField(client, row.entryId, { description: row.recommendedDescription });
|
|
847
|
-
row.applied = 'description';
|
|
848
|
-
}
|
|
849
|
-
else if (row.contentType === 'articleType' || row.contentType === 'customType') {
|
|
850
|
-
if (row.recommendedDescription && row.recommendedDescription !== row.currentDescription) {
|
|
851
|
-
await updateEntryField(client, row.entryId, {
|
|
852
|
-
indexPageDescription: row.recommendedDescription,
|
|
853
|
-
});
|
|
854
|
-
row.applied = 'indexPageDescription';
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
else if (row.contentType === 'tag' &&
|
|
858
|
-
row.recommendedDescription &&
|
|
859
|
-
row.recommendedDescription !== row.currentDescription) {
|
|
860
|
-
await updateEntryField(client, row.entryId, { description: row.recommendedDescription });
|
|
861
|
-
row.applied = 'description';
|
|
862
|
-
}
|
|
863
|
-
else if (row.contentType === 'person' &&
|
|
864
|
-
row.recommendedDescription &&
|
|
865
|
-
row.recommendedDescription !== row.currentDescription) {
|
|
866
|
-
await updateEntryField(client, row.entryId, { description: row.recommendedDescription });
|
|
867
|
-
row.applied = 'description';
|
|
868
|
-
}
|
|
869
|
-
else if (row.contentType === 'article' &&
|
|
870
|
-
row.recommendedDescription &&
|
|
871
|
-
row.recommendedDescription !== row.currentDescription) {
|
|
872
|
-
await updateEntryField(client, row.entryId, { description: row.recommendedDescription });
|
|
873
|
-
row.applied = 'description';
|
|
874
|
-
}
|
|
875
|
-
if (row.contentType === 'page') {
|
|
876
|
-
const entry = (await client.entry.get({ entryId: row.entryId }));
|
|
877
|
-
const existing = linkIds(entry, 'structuredData');
|
|
878
|
-
const canonical = [webPageId, websiteId];
|
|
879
|
-
if (!sameIdSet(existing, canonical)) {
|
|
880
|
-
await linkSchemaArray(client, row.entryId, 'structuredData', canonical);
|
|
881
|
-
row.applied = row.applied ? `${row.applied}; schema` : 'schema';
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
if (row.contentType === 'tag') {
|
|
885
|
-
const entry = (await client.entry.get({ entryId: row.entryId }));
|
|
886
|
-
const existing = linkIds(entry, 'structuredData');
|
|
887
|
-
const canonical = [collectionId];
|
|
888
|
-
if (!sameIdSet(existing, canonical)) {
|
|
889
|
-
await linkSchemaArray(client, row.entryId, 'structuredData', canonical);
|
|
890
|
-
row.applied = row.applied ? `${row.applied}; schema` : 'schema';
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
if (row.contentType === 'customType') {
|
|
894
|
-
const entry = (await client.entry.get({ entryId: row.entryId }));
|
|
895
|
-
const existing = linkIds(entry, 'indexPageStructuredData');
|
|
896
|
-
const canonical = [collectionId];
|
|
897
|
-
if (!sameIdSet(existing, canonical)) {
|
|
898
|
-
await linkSchemaArray(client, row.entryId, 'indexPageStructuredData', canonical);
|
|
899
|
-
row.applied = row.applied ? `${row.applied}; schema` : 'schema';
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
if (row.contentType === 'person') {
|
|
903
|
-
const entry = (await client.entry.get({ entryId: row.entryId }));
|
|
904
|
-
const existing = linkIds(entry, 'structuredData');
|
|
905
|
-
const canonical = [profilePageId];
|
|
906
|
-
if (!sameIdSet(existing, canonical)) {
|
|
907
|
-
await linkSchemaArray(client, row.entryId, 'structuredData', canonical);
|
|
908
|
-
row.applied = row.applied ? `${row.applied}; schema` : 'schema';
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
catch (error) {
|
|
913
|
-
row.applied = `error: ${error instanceof Error ? error.message : String(error)}`;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
export function parseSeoAuditArgv(argv) {
|
|
918
|
-
const featuredImagesOnly = argv.includes('--featured-images-only');
|
|
919
|
-
return {
|
|
920
|
-
apply: argv.includes('--apply'),
|
|
921
|
-
sampleArticlesOnly: argv.includes('--sample-articles'),
|
|
922
|
-
featuredImages: featuredImagesOnly || argv.includes('--featured-images'),
|
|
923
|
-
featuredImagesOnly,
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
//# sourceMappingURL=seo-audit.js.map
|