@cms-lab/core 1.0.3 → 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 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 WordPress SEO fields, while image checks recognize native
15
- alt fields such as Strapi `alternativeText`, Directus file `description`, and
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 CmsProviderConfig = PrismicCmsProviderConfig | StrapiCmsProviderConfig | DirectusCmsProviderConfig | WordPressCmsProviderConfig;
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, and WordPress SEO plugin JSON.",
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, and WordPress alt_text are checked.",
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 = new URL(candidate.path, siteUrl);
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 = asRecord(document.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(readDataPath(document.data, rule.path))) {
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(readDataPath(data, path))
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 = asRecord(value);
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 asRecord(value) {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cms-lab/core",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "Core config, scan, diagnostics, and checks for cms-lab.",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/i-afaqrashid/cms-lab.git",
10
10
  "directory": "packages/core"
11
11
  },
12
- "homepage": "https://cms-lab.dev",
12
+ "homepage": "https://cmslab.afaqrashid.com",
13
13
  "bugs": {
14
14
  "url": "https://github.com/i-afaqrashid/cms-lab/issues"
15
15
  },