@cms-lab/core 1.0.4 → 1.0.5
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/README.md +7 -3
- package/dist/index.d.ts +37 -5
- package/dist/index.js +99 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,9 +11,13 @@ typed config files, tests, and adapter/report integrations.
|
|
|
11
11
|
|
|
12
12
|
`scanDocuments` understands the shared `CMSDocument` contract plus provider
|
|
13
13
|
field shapes from the bundled adapters. SEO checks recognize common Prismic,
|
|
14
|
-
Strapi, Directus, and
|
|
15
|
-
alt fields such as Strapi `alternativeText`, Directus
|
|
16
|
-
WordPress `alt_text
|
|
14
|
+
Strapi, Directus, WordPress, Contentful, and Sanity SEO fields, while image
|
|
15
|
+
checks recognize native alt fields such as Strapi `alternativeText`, Directus
|
|
16
|
+
file `description`, WordPress `alt_text`, Contentful asset descriptions, and
|
|
17
|
+
Sanity image `alt` fields.
|
|
18
|
+
|
|
19
|
+
`readCmsDataPath` is exported for adapters that need to read dotted paths from
|
|
20
|
+
normalized CMS payloads, for example custom `uidField` and `urlField` mapping.
|
|
17
21
|
|
|
18
22
|
## Open Source
|
|
19
23
|
|
package/dist/index.d.ts
CHANGED
|
@@ -45,10 +45,14 @@ type PrismicCmsProviderConfig = {
|
|
|
45
45
|
accessToken?: string;
|
|
46
46
|
endpoint?: string;
|
|
47
47
|
};
|
|
48
|
+
type CmsFieldMappingConfig = {
|
|
49
|
+
uidField?: string;
|
|
50
|
+
urlField?: string;
|
|
51
|
+
};
|
|
48
52
|
type StrapiCollectionConfig = {
|
|
49
53
|
type: string;
|
|
50
54
|
endpoint: string;
|
|
51
|
-
};
|
|
55
|
+
} & CmsFieldMappingConfig;
|
|
52
56
|
type StrapiCmsProviderConfig = {
|
|
53
57
|
provider: "strapi";
|
|
54
58
|
url: string;
|
|
@@ -58,7 +62,7 @@ type StrapiCmsProviderConfig = {
|
|
|
58
62
|
type DirectusCollectionConfig = {
|
|
59
63
|
type: string;
|
|
60
64
|
collection: string;
|
|
61
|
-
};
|
|
65
|
+
} & CmsFieldMappingConfig;
|
|
62
66
|
type DirectusCmsProviderConfig = {
|
|
63
67
|
provider: "directus";
|
|
64
68
|
url: string;
|
|
@@ -68,13 +72,39 @@ type DirectusCmsProviderConfig = {
|
|
|
68
72
|
type WordPressContentTypeConfig = {
|
|
69
73
|
type: string;
|
|
70
74
|
endpoint: string;
|
|
71
|
-
};
|
|
75
|
+
} & CmsFieldMappingConfig;
|
|
72
76
|
type WordPressCmsProviderConfig = {
|
|
73
77
|
provider: "wordpress";
|
|
74
78
|
url: string;
|
|
75
79
|
contentTypes?: WordPressContentTypeConfig[];
|
|
76
80
|
};
|
|
77
|
-
type
|
|
81
|
+
type ContentfulContentTypeConfig = {
|
|
82
|
+
type: string;
|
|
83
|
+
contentType: string;
|
|
84
|
+
} & CmsFieldMappingConfig;
|
|
85
|
+
type ContentfulCmsProviderConfig = {
|
|
86
|
+
provider: "contentful";
|
|
87
|
+
spaceId: string;
|
|
88
|
+
accessToken: string;
|
|
89
|
+
environment?: string;
|
|
90
|
+
apiUrl?: string;
|
|
91
|
+
contentTypes: ContentfulContentTypeConfig[];
|
|
92
|
+
};
|
|
93
|
+
type SanityContentTypeConfig = {
|
|
94
|
+
type: string;
|
|
95
|
+
documentType: string;
|
|
96
|
+
} & CmsFieldMappingConfig;
|
|
97
|
+
type SanityCmsProviderConfig = {
|
|
98
|
+
provider: "sanity";
|
|
99
|
+
projectId: string;
|
|
100
|
+
dataset: string;
|
|
101
|
+
apiVersion?: string;
|
|
102
|
+
token?: string;
|
|
103
|
+
useCdn?: boolean;
|
|
104
|
+
perspective?: "published" | "drafts" | "raw";
|
|
105
|
+
contentTypes: SanityContentTypeConfig[];
|
|
106
|
+
};
|
|
107
|
+
type CmsProviderConfig = PrismicCmsProviderConfig | StrapiCmsProviderConfig | DirectusCmsProviderConfig | WordPressCmsProviderConfig | ContentfulCmsProviderConfig | SanityCmsProviderConfig;
|
|
78
108
|
type CmsLabConfig = {
|
|
79
109
|
site: {
|
|
80
110
|
url: string;
|
|
@@ -130,6 +160,8 @@ declare function loadCmsLabConfig(options: {
|
|
|
130
160
|
configPath?: string;
|
|
131
161
|
}): Promise<LoadedCmsLabConfig>;
|
|
132
162
|
|
|
163
|
+
declare function readCmsDataPath(data: unknown, path: string): unknown;
|
|
164
|
+
|
|
133
165
|
declare function createDiagnostic(input: Diagnostic): Diagnostic;
|
|
134
166
|
declare function summarizeDiagnostics(diagnostics: Diagnostic[]): ScanSummary;
|
|
135
167
|
declare function explainDiagnostic(code: string): DiagnosticExplanation | undefined;
|
|
@@ -161,4 +193,4 @@ type ScanDocumentsOptions = {
|
|
|
161
193
|
};
|
|
162
194
|
declare function scanDocuments(options: ScanDocumentsOptions): Promise<ScanResult>;
|
|
163
195
|
|
|
164
|
-
export { type CMSDocument, type CMSDocumentStatus, type CheckGroup, CmsFetchError, type CmsLabConfig, CmsLabError, type CmsProviderConfig, ConfigLoadError, type Diagnostic, type DiagnosticExplanation, type DiagnosticSeverity, type DirectusCmsProviderConfig, type DirectusCollectionConfig, type FetchLike, type LoadedCmsLabConfig, type PrismicCmsProviderConfig, type ProjectInfo, type RequiredFieldRule, type RouteDefinition, type ScanDocumentsOptions, type ScanFilters, type ScanResult, type ScanSummary, SiteUnreachableError, type StrapiCmsProviderConfig, type StrapiCollectionConfig, type WordPressCmsProviderConfig, type WordPressContentTypeConfig, createDiagnostic, defineConfig, explainDiagnostic, listDiagnosticExplanations, loadCmsLabConfig, scanDocuments, summarizeDiagnostics, validateConfig };
|
|
196
|
+
export { type CMSDocument, type CMSDocumentStatus, type CheckGroup, CmsFetchError, type CmsFieldMappingConfig, type CmsLabConfig, CmsLabError, type CmsProviderConfig, ConfigLoadError, type ContentfulCmsProviderConfig, type ContentfulContentTypeConfig, type Diagnostic, type DiagnosticExplanation, type DiagnosticSeverity, type DirectusCmsProviderConfig, type DirectusCollectionConfig, type FetchLike, type LoadedCmsLabConfig, type PrismicCmsProviderConfig, type ProjectInfo, type RequiredFieldRule, type RouteDefinition, type SanityCmsProviderConfig, type SanityContentTypeConfig, type ScanDocumentsOptions, type ScanFilters, type ScanResult, type ScanSummary, SiteUnreachableError, type StrapiCmsProviderConfig, type StrapiCollectionConfig, type WordPressCmsProviderConfig, type WordPressContentTypeConfig, createDiagnostic, defineConfig, explainDiagnostic, listDiagnosticExplanations, loadCmsLabConfig, readCmsDataPath, scanDocuments, summarizeDiagnostics, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,10 @@ var prismicConfigSchema = z.object({
|
|
|
49
49
|
accessToken: z.string().optional(),
|
|
50
50
|
endpoint: z.string().url().optional()
|
|
51
51
|
}).strict();
|
|
52
|
+
var cmsFieldMappingShape = {
|
|
53
|
+
uidField: z.string().min(1).optional(),
|
|
54
|
+
urlField: z.string().min(1).optional()
|
|
55
|
+
};
|
|
52
56
|
var strapiConfigSchema = z.object({
|
|
53
57
|
provider: z.literal("strapi"),
|
|
54
58
|
url: z.string().url(),
|
|
@@ -56,7 +60,8 @@ var strapiConfigSchema = z.object({
|
|
|
56
60
|
collections: z.array(
|
|
57
61
|
z.object({
|
|
58
62
|
type: z.string().min(1),
|
|
59
|
-
endpoint: z.string().min(1)
|
|
63
|
+
endpoint: z.string().min(1),
|
|
64
|
+
...cmsFieldMappingShape
|
|
60
65
|
}).strict()
|
|
61
66
|
).min(1)
|
|
62
67
|
}).strict();
|
|
@@ -67,7 +72,8 @@ var directusConfigSchema = z.object({
|
|
|
67
72
|
collections: z.array(
|
|
68
73
|
z.object({
|
|
69
74
|
type: z.string().min(1),
|
|
70
|
-
collection: z.string().min(1)
|
|
75
|
+
collection: z.string().min(1),
|
|
76
|
+
...cmsFieldMappingShape
|
|
71
77
|
}).strict()
|
|
72
78
|
).min(1)
|
|
73
79
|
}).strict();
|
|
@@ -77,15 +83,48 @@ var wordpressConfigSchema = z.object({
|
|
|
77
83
|
contentTypes: z.array(
|
|
78
84
|
z.object({
|
|
79
85
|
type: z.string().min(1),
|
|
80
|
-
endpoint: z.string().min(1)
|
|
86
|
+
endpoint: z.string().min(1),
|
|
87
|
+
...cmsFieldMappingShape
|
|
81
88
|
}).strict()
|
|
82
89
|
).optional()
|
|
83
90
|
}).strict();
|
|
91
|
+
var contentfulConfigSchema = z.object({
|
|
92
|
+
provider: z.literal("contentful"),
|
|
93
|
+
spaceId: z.string().min(1),
|
|
94
|
+
accessToken: z.string().min(1),
|
|
95
|
+
environment: z.string().min(1).optional(),
|
|
96
|
+
apiUrl: z.string().url().optional(),
|
|
97
|
+
contentTypes: z.array(
|
|
98
|
+
z.object({
|
|
99
|
+
type: z.string().min(1),
|
|
100
|
+
contentType: z.string().min(1),
|
|
101
|
+
...cmsFieldMappingShape
|
|
102
|
+
}).strict()
|
|
103
|
+
).min(1)
|
|
104
|
+
}).strict();
|
|
105
|
+
var sanityConfigSchema = z.object({
|
|
106
|
+
provider: z.literal("sanity"),
|
|
107
|
+
projectId: z.string().min(1),
|
|
108
|
+
dataset: z.string().min(1),
|
|
109
|
+
apiVersion: z.string().min(1).optional(),
|
|
110
|
+
token: z.string().optional(),
|
|
111
|
+
useCdn: z.boolean().optional(),
|
|
112
|
+
perspective: z.enum(["published", "drafts", "raw"]).optional(),
|
|
113
|
+
contentTypes: z.array(
|
|
114
|
+
z.object({
|
|
115
|
+
type: z.string().min(1),
|
|
116
|
+
documentType: z.string().min(1),
|
|
117
|
+
...cmsFieldMappingShape
|
|
118
|
+
}).strict()
|
|
119
|
+
).min(1)
|
|
120
|
+
}).strict();
|
|
84
121
|
var cmsConfigSchema = z.discriminatedUnion("provider", [
|
|
85
122
|
prismicConfigSchema,
|
|
86
123
|
strapiConfigSchema,
|
|
87
124
|
directusConfigSchema,
|
|
88
|
-
wordpressConfigSchema
|
|
125
|
+
wordpressConfigSchema,
|
|
126
|
+
contentfulConfigSchema,
|
|
127
|
+
sanityConfigSchema
|
|
89
128
|
]);
|
|
90
129
|
var requiredFieldRuleSchema = z.object({
|
|
91
130
|
type: z.string().min(1),
|
|
@@ -168,6 +207,28 @@ function selfEntryPath() {
|
|
|
168
207
|
return join(dirname(currentFile), `index.${extension}`);
|
|
169
208
|
}
|
|
170
209
|
|
|
210
|
+
// src/data-path.ts
|
|
211
|
+
function readCmsDataPath(data, path) {
|
|
212
|
+
let current = data;
|
|
213
|
+
for (const segment of path.split(".")) {
|
|
214
|
+
if (!segment) {
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
const record = asRecord(current);
|
|
218
|
+
if (!record || !(segment in record)) {
|
|
219
|
+
return void 0;
|
|
220
|
+
}
|
|
221
|
+
current = record[segment];
|
|
222
|
+
}
|
|
223
|
+
return current;
|
|
224
|
+
}
|
|
225
|
+
function asRecord(value) {
|
|
226
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
171
232
|
// src/diagnostics.ts
|
|
172
233
|
function createDiagnostic(input) {
|
|
173
234
|
return input;
|
|
@@ -236,14 +297,14 @@ var explanations = [
|
|
|
236
297
|
code: "SEO-META-MISSING",
|
|
237
298
|
severity: "warning",
|
|
238
299
|
title: "SEO metadata is missing",
|
|
239
|
-
meaning: "A document is missing one or more configured SEO fields. cms-lab checks common provider fields such as Prismic meta fields, Strapi/Directus seo objects,
|
|
300
|
+
meaning: "A document is missing one or more configured SEO fields. cms-lab checks common provider fields such as Prismic meta fields, Strapi/Directus seo objects, WordPress SEO plugin JSON, and Contentful/Sanity SEO fields.",
|
|
240
301
|
fix: "Fill the missing CMS fields or disable the SEO check if this content type intentionally does not use metadata."
|
|
241
302
|
},
|
|
242
303
|
{
|
|
243
304
|
code: "A11Y-IMG-ALT",
|
|
244
305
|
severity: "warning",
|
|
245
306
|
title: "Image alt text is missing",
|
|
246
|
-
meaning: "An image-like CMS field has no useful alt text, or uses a placeholder such as image, photo, or picture. Native fields such as Prismic alt, Strapi alternativeText, Directus file description,
|
|
307
|
+
meaning: "An image-like CMS field has no useful alt text, or uses a placeholder such as image, photo, or picture. Native fields such as Prismic alt, Strapi alternativeText, Directus file description, WordPress alt_text, Contentful asset descriptions, and Sanity image alt fields are checked.",
|
|
247
308
|
fix: "Add meaningful alt text in the CMS, or leave decorative images out of content fields that require editorial alt text."
|
|
248
309
|
},
|
|
249
310
|
{
|
|
@@ -372,7 +433,7 @@ async function checkRouteReachability(config, candidates, fetchImpl, timeoutMs,
|
|
|
372
433
|
const results = await mapLimit(candidates, concurrency, async (candidate) => {
|
|
373
434
|
const diagnostics = [];
|
|
374
435
|
const siteUrl = new URL(config.site.url);
|
|
375
|
-
const url =
|
|
436
|
+
const url = resolveSiteRouteUrl(siteUrl, candidate.path);
|
|
376
437
|
const diagnosticPath = pathForDiagnostic(candidate.path);
|
|
377
438
|
let response;
|
|
378
439
|
if (url.origin !== siteUrl.origin) {
|
|
@@ -441,6 +502,13 @@ async function checkRouteReachability(config, candidates, fetchImpl, timeoutMs,
|
|
|
441
502
|
});
|
|
442
503
|
return results.flat();
|
|
443
504
|
}
|
|
505
|
+
function resolveSiteRouteUrl(siteUrl, path) {
|
|
506
|
+
const url = new URL(path, siteUrl);
|
|
507
|
+
if (!url.search && siteUrl.search) {
|
|
508
|
+
url.search = siteUrl.search;
|
|
509
|
+
}
|
|
510
|
+
return url;
|
|
511
|
+
}
|
|
444
512
|
async function assertSiteReachable(siteUrl, fetchImpl, timeoutMs, retries) {
|
|
445
513
|
try {
|
|
446
514
|
const response = await fetchWithRetries(
|
|
@@ -496,7 +564,7 @@ function checkSeoFields(config, documents) {
|
|
|
496
564
|
const checkMetaTitle = typeof seo === "object" ? seo.metaTitle !== false : true;
|
|
497
565
|
const checkMetaDescription = typeof seo === "object" ? seo.metaDescription !== false : true;
|
|
498
566
|
for (const document of documents) {
|
|
499
|
-
const data =
|
|
567
|
+
const data = asRecord2(document.data);
|
|
500
568
|
if (!data) {
|
|
501
569
|
continue;
|
|
502
570
|
}
|
|
@@ -552,7 +620,7 @@ function checkRequiredFields(config, documents) {
|
|
|
552
620
|
continue;
|
|
553
621
|
}
|
|
554
622
|
const fullPath = `data.${rule.path}`;
|
|
555
|
-
if (!isMissingFieldValue(
|
|
623
|
+
if (!isMissingFieldValue(readCmsDataPath(document.data, rule.path))) {
|
|
556
624
|
continue;
|
|
557
625
|
}
|
|
558
626
|
diagnostics.push(
|
|
@@ -577,7 +645,7 @@ function requiredFieldRules(config) {
|
|
|
577
645
|
}
|
|
578
646
|
function hasSeoValue(config, data, kind) {
|
|
579
647
|
return seoFieldPaths(config.cms.provider, kind).some(
|
|
580
|
-
(path) => !isBlank(
|
|
648
|
+
(path) => !isBlank(readCmsDataPath(data, path))
|
|
581
649
|
);
|
|
582
650
|
}
|
|
583
651
|
function seoFieldPaths(provider, kind) {
|
|
@@ -624,7 +692,7 @@ function collectImagesMissingAlt(value, provider, path = "data") {
|
|
|
624
692
|
(item, index) => collectImagesMissingAlt(item, provider, `${path}[${index}]`)
|
|
625
693
|
);
|
|
626
694
|
}
|
|
627
|
-
const record =
|
|
695
|
+
const record = asRecord2(value);
|
|
628
696
|
if (!record) {
|
|
629
697
|
return [];
|
|
630
698
|
}
|
|
@@ -649,6 +717,12 @@ function imageAltCandidate(provider, record) {
|
|
|
649
717
|
if (provider === "directus" && isDirectusImageRecord(record)) {
|
|
650
718
|
return { isImage: true, value: record.description };
|
|
651
719
|
}
|
|
720
|
+
if (provider === "contentful" && isContentfulImageRecord(record)) {
|
|
721
|
+
return { isImage: true, value: record.description ?? record.title };
|
|
722
|
+
}
|
|
723
|
+
if (provider === "sanity" && "asset" in record && (asRecord2(record.asset)?._ref || asRecord2(record.asset)?._id)) {
|
|
724
|
+
return { isImage: true, value: record.alt };
|
|
725
|
+
}
|
|
652
726
|
return { isImage: false, value: void 0 };
|
|
653
727
|
}
|
|
654
728
|
function isDirectusImageRecord(record) {
|
|
@@ -657,6 +731,18 @@ function isDirectusImageRecord(record) {
|
|
|
657
731
|
function hasImageExtension(value) {
|
|
658
732
|
return typeof value === "string" && /\.(?:avif|gif|jpe?g|png|svg|webp)$/i.test(value);
|
|
659
733
|
}
|
|
734
|
+
function isContentfulImageRecord(record) {
|
|
735
|
+
const file = asRecord2(record.file);
|
|
736
|
+
return typeof file?.url === "string" && (isContentfulImageHost(file.url) || hasImageExtension(file.url)) || hasImageExtension(file?.fileName);
|
|
737
|
+
}
|
|
738
|
+
function isContentfulImageHost(value) {
|
|
739
|
+
const normalized = value.startsWith("//") ? `https:${value}` : value;
|
|
740
|
+
try {
|
|
741
|
+
return new URL(normalized).hostname === "images.ctfassets.net";
|
|
742
|
+
} catch {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
660
746
|
function isCheckEnabled(value, defaultValue) {
|
|
661
747
|
if (typeof value === "boolean") {
|
|
662
748
|
return value;
|
|
@@ -794,24 +880,10 @@ function isMissingFieldValue(value) {
|
|
|
794
880
|
}
|
|
795
881
|
return false;
|
|
796
882
|
}
|
|
797
|
-
function readDataPath(data, path) {
|
|
798
|
-
let current = data;
|
|
799
|
-
for (const segment of path.split(".")) {
|
|
800
|
-
if (!segment) {
|
|
801
|
-
return void 0;
|
|
802
|
-
}
|
|
803
|
-
const record = asRecord(current);
|
|
804
|
-
if (!record || !(segment in record)) {
|
|
805
|
-
return void 0;
|
|
806
|
-
}
|
|
807
|
-
current = record[segment];
|
|
808
|
-
}
|
|
809
|
-
return current;
|
|
810
|
-
}
|
|
811
883
|
function isBlankOrPlaceholderAlt(value) {
|
|
812
884
|
return isBlank(value) || typeof value === "string" && ["image", "photo", "picture"].includes(value.trim().toLowerCase());
|
|
813
885
|
}
|
|
814
|
-
function
|
|
886
|
+
function asRecord2(value) {
|
|
815
887
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
816
888
|
return value;
|
|
817
889
|
}
|
|
@@ -838,6 +910,7 @@ export {
|
|
|
838
910
|
explainDiagnostic,
|
|
839
911
|
listDiagnosticExplanations,
|
|
840
912
|
loadCmsLabConfig,
|
|
913
|
+
readCmsDataPath,
|
|
841
914
|
scanDocuments,
|
|
842
915
|
summarizeDiagnostics,
|
|
843
916
|
validateConfig
|