schemaorg-kit 1.1.1 → 1.3.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/dist/index.mjs CHANGED
@@ -56,6 +56,8 @@ import { z as z3 } from "zod";
56
56
  import { z as z2 } from "zod";
57
57
  var PersonOrOrgRef = z2.union([
58
58
  z2.string(),
59
+ // @id-only reference for @graph cross-referencing (e.g. { "@id": "https://example.com/#organization" })
60
+ z2.object({ "@id": z2.string() }),
59
61
  // Accept any Person or Organization object — including full schema outputs via .toObject().
60
62
  // .catchall() allows extra fields; name/url are optional to match ThingSchema's base types.
61
63
  z2.object({ "@type": z2.literal("Person") }).catchall(z2.unknown()),
@@ -65,6 +67,7 @@ var PersonOrOrgRef = z2.union([
65
67
  // src/types/shared/ImageObject.ts
66
68
  var ImageObjectSchema = z3.object({
67
69
  "@type": z3.literal("ImageObject").default("ImageObject"),
70
+ "@id": z3.string().optional(),
68
71
  url: z3.url(),
69
72
  contentUrl: z3.url().optional(),
70
73
  width: z3.union([
@@ -121,8 +124,9 @@ var ThingSchema = z4.object({
121
124
  sameAs: z4.union([z4.url(), z4.array(z4.url())]).optional(),
122
125
  alternateName: z4.string().optional(),
123
126
  // Relationships (loose refs to avoid circular type inference)
124
- subjectOf: AnyThingRef.optional(),
125
- mainEntityOfPage: z4.union([z4.url(), AnyThingRef]).optional()
127
+ // All accept { "@id": "..." } for @graph cross-referencing
128
+ subjectOf: z4.union([z4.object({ "@id": z4.string() }), AnyThingRef]).optional(),
129
+ mainEntityOfPage: z4.union([z4.url(), z4.object({ "@id": z4.string() }), AnyThingRef]).optional()
126
130
  });
127
131
  function extendThing(type, shape) {
128
132
  return ThingSchema.extend({
@@ -180,6 +184,7 @@ import { z as z15 } from "zod";
180
184
  import { z as z8 } from "zod";
181
185
  var ContactPointSchema = z8.object({
182
186
  "@type": z8.literal("ContactPoint").default("ContactPoint"),
187
+ "@id": z8.string().optional(),
183
188
  telephone: z8.string().optional(),
184
189
  contactType: z8.string().optional(),
185
190
  // e.g. "customer support", "sales"
@@ -381,7 +386,10 @@ var OfferSchema = z12.object({
381
386
  validThrough: z12.string().optional(),
382
387
  // ISO 8601
383
388
  url: z12.url().optional(),
384
- seller: z12.object({ "@type": z12.string(), name: z12.string() }).optional(),
389
+ seller: z12.union([
390
+ z12.object({ "@id": z12.string() }),
391
+ z12.object({ "@type": z12.string(), name: z12.string() })
392
+ ]).optional(),
385
393
  category: z12.string().optional(),
386
394
  inventoryLevel: z12.object({
387
395
  "@type": z12.literal("QuantitativeValue").default("QuantitativeValue"),
@@ -394,7 +402,10 @@ var OfferSchema = z12.object({
394
402
  z12.array(UnitPriceSpecificationSchema)
395
403
  ]).optional(),
396
404
  shippingDetails: z12.union([OfferShippingDetailsSchema, z12.array(OfferShippingDetailsSchema)]).optional(),
397
- hasMerchantReturnPolicy: z12.lazy(() => z12.object({ "@type": z12.string() }).catchall(z12.unknown())).optional()
405
+ hasMerchantReturnPolicy: z12.union([
406
+ z12.object({ "@id": z12.string() }),
407
+ z12.lazy(() => z12.object({ "@type": z12.string() }).catchall(z12.unknown()))
408
+ ]).optional()
398
409
  });
399
410
  var MerchantReturnPolicySchema = z12.object({
400
411
  "@type": z12.literal("MerchantReturnPolicy").default("MerchantReturnPolicy"),
@@ -573,8 +584,9 @@ var OrganizationSchema = extendThing("Organization", {
573
584
  interactionStatistic: z15.union([InteractionCounterSchema, z15.array(InteractionCounterSchema)]).optional(),
574
585
  agentInteractionStatistic: z15.union([InteractionCounterSchema, z15.array(InteractionCounterSchema)]).optional(),
575
586
  // Hierarchical organization (loose refs to avoid circular type inference):
576
- subOrganization: NestedOrgRef.optional(),
577
- parentOrganization: NestedOrgRef.optional(),
587
+ // Accept { "@id": "..." } for @graph cross-referencing
588
+ subOrganization: z15.union([z15.object({ "@id": z15.string() }), NestedOrgRef]).optional(),
589
+ parentOrganization: z15.union([z15.object({ "@id": z15.string() }), NestedOrgRef]).optional(),
578
590
  sameAs: z15.union([z15.url(), z15.array(z15.url())]).optional()
579
591
  });
580
592
  var createOrganization = makeFactory(OrganizationSchema);
@@ -627,7 +639,10 @@ var ReviewSchema = z16.object({
627
639
  // ISO 8601 date
628
640
  publisher: PersonOrOrgRef.optional(),
629
641
  // itemReviewed: any Thing — kept loose to avoid circular imports
630
- itemReviewed: z16.lazy(() => z16.object({ "@type": z16.string() }).passthrough()).optional(),
642
+ itemReviewed: z16.union([
643
+ z16.object({ "@id": z16.string() }),
644
+ z16.lazy(() => z16.object({ "@type": z16.string() }).catchall(z16.unknown()))
645
+ ]).optional(),
631
646
  name: z16.string().optional(),
632
647
  // Review headline
633
648
  url: z16.url().optional(),
@@ -636,9 +651,12 @@ var ReviewSchema = z16.object({
636
651
  });
637
652
  var EmployerAggregateRatingSchema = AggregateRatingSchema.extend({
638
653
  "@type": z16.literal("EmployerAggregateRating").default("EmployerAggregateRating"),
639
- itemReviewed: z16.lazy(
640
- () => z16.object({ "@type": z16.string(), name: z16.string() }).passthrough()
641
- ).optional()
654
+ itemReviewed: z16.union([
655
+ z16.object({ "@id": z16.string() }),
656
+ z16.lazy(
657
+ () => z16.object({ "@type": z16.string(), name: z16.string() }).catchall(z16.unknown())
658
+ )
659
+ ]).optional()
642
660
  });
643
661
 
644
662
  // src/types/things/Product.ts
@@ -852,6 +870,7 @@ var EventAttendanceMode = z19.enum([
852
870
  ]).transform((v) => `https://schema.org/${v}`);
853
871
  var PlaceRef = z19.union([
854
872
  z19.string(),
873
+ z19.object({ "@id": z19.string() }),
855
874
  z19.object({
856
875
  "@type": z19.literal("Place").default("Place"),
857
876
  name: z19.string().optional(),
@@ -884,11 +903,17 @@ var EventSchema = extendThing("Event", {
884
903
  previousStartDate: z19.string().optional(),
885
904
  // ISO 8601 — for rescheduled events
886
905
  aggregateRating: AggregateRatingSchema.optional(),
887
- // Sub-events:
906
+ // Sub-events (accept @id refs for @graph cross-referencing):
888
907
  subEvent: z19.lazy(
889
- () => z19.array(z19.object({ "@type": z19.string() }).catchall(z19.unknown()))
908
+ () => z19.array(z19.union([
909
+ z19.object({ "@id": z19.string() }),
910
+ z19.object({ "@type": z19.string() }).catchall(z19.unknown())
911
+ ]))
890
912
  ).optional(),
891
- superEvent: z19.lazy(() => z19.object({ "@type": z19.string() }).catchall(z19.unknown())).optional(),
913
+ superEvent: z19.union([
914
+ z19.object({ "@id": z19.string() }),
915
+ z19.lazy(() => z19.object({ "@type": z19.string() }).catchall(z19.unknown()))
916
+ ]).optional(),
892
917
  // Additional:
893
918
  inLanguage: z19.union([
894
919
  z19.string(),
@@ -943,9 +968,12 @@ var PlaceSchema = extendThing("Place", {
943
968
  value: z21.union([z21.boolean(), z21.string(), z21.number()])
944
969
  })
945
970
  ).optional(),
946
- containedInPlace: z21.lazy(
947
- () => z21.object({ "@type": z21.string(), name: z21.string().optional() }).catchall(z21.unknown())
948
- ).optional()
971
+ containedInPlace: z21.union([
972
+ z21.object({ "@id": z21.string() }),
973
+ z21.lazy(
974
+ () => z21.object({ "@type": z21.string(), name: z21.string().optional() }).catchall(z21.unknown())
975
+ )
976
+ ]).optional()
949
977
  });
950
978
  var createPlace = makeFactory(PlaceSchema);
951
979
 
@@ -996,11 +1024,15 @@ var LocalBusinessSchema = PlaceSchema.extend({
996
1024
  iso6523Code: z22.string().optional(),
997
1025
  hasMerchantReturnPolicy: MerchantReturnPolicySchema.optional(),
998
1026
  sameAs: z22.union([z22.url(), z22.array(z22.url())]).optional(),
999
- // Departments (nested LocalBusiness):
1027
+ // Departments (nested LocalBusiness — accept @id refs for @graph cross-referencing):
1000
1028
  department: z22.lazy(
1001
1029
  () => z22.union([
1030
+ z22.object({ "@id": z22.string() }),
1002
1031
  z22.object({ "@type": z22.string() }).catchall(z22.unknown()),
1003
- z22.array(z22.object({ "@type": z22.string() }).catchall(z22.unknown()))
1032
+ z22.array(z22.union([
1033
+ z22.object({ "@id": z22.string() }),
1034
+ z22.object({ "@type": z22.string() }).catchall(z22.unknown())
1035
+ ]))
1004
1036
  ])
1005
1037
  ).optional(),
1006
1038
  // Opening hours in string format (alternative to openingHoursSpecification):
@@ -1140,12 +1172,15 @@ var BookSchema = z24.object({
1140
1172
  sameAs: z24.union([z24.string(), z24.array(z24.string())]).optional(),
1141
1173
  /** Specific editions — each may have ReadAction / BorrowAction */
1142
1174
  workExample: z24.union([BookEditionSchema, z24.array(BookEditionSchema)]).optional(),
1143
- /** Publisher of the primary edition */
1144
- publisher: z24.object({
1145
- "@type": z24.union([z24.literal("Organization"), z24.literal("Person")]),
1146
- name: z24.string(),
1147
- url: z24.url().optional()
1148
- }).optional(),
1175
+ /** Publisher of the primary edition (accept @id refs for @graph cross-referencing) */
1176
+ publisher: z24.union([
1177
+ z24.object({ "@id": z24.string() }),
1178
+ z24.object({
1179
+ "@type": z24.union([z24.literal("Organization"), z24.literal("Person")]),
1180
+ name: z24.string(),
1181
+ url: z24.url().optional()
1182
+ })
1183
+ ]).optional(),
1149
1184
  /** Number of pages */
1150
1185
  numberOfPages: z24.number().int().positive().optional(),
1151
1186
  /** Original publication year */
@@ -1155,7 +1190,10 @@ var BookSchema = z24.object({
1155
1190
  /** BCP 47 language tag */
1156
1191
  inLanguage: z24.string().optional(),
1157
1192
  /** Aggregate rating across editions */
1158
- aggregateRating: z24.lazy(() => z24.object({ "@type": z24.string() }).catchall(z24.unknown())).optional()
1193
+ aggregateRating: z24.union([
1194
+ z24.object({ "@id": z24.string() }),
1195
+ z24.lazy(() => z24.object({ "@type": z24.string() }).catchall(z24.unknown()))
1196
+ ]).optional()
1159
1197
  });
1160
1198
  var createBook = makeFactory(BookSchema);
1161
1199
 
@@ -1194,11 +1232,14 @@ var CreativeWorkSchema = extendThing("CreativeWork", {
1194
1232
  cssSelector: z25.string().optional()
1195
1233
  })
1196
1234
  ).optional(),
1197
- // For subscription / paywalled content
1198
- isPartOf: z25.object({
1199
- "@type": z25.string(),
1200
- name: z25.string().optional()
1201
- }).optional(),
1235
+ // For subscription / paywalled content (accept @id refs for @graph cross-referencing)
1236
+ isPartOf: z25.union([
1237
+ z25.object({ "@id": z25.string() }),
1238
+ z25.object({
1239
+ "@type": z25.string(),
1240
+ name: z25.string().optional()
1241
+ })
1242
+ ]).optional(),
1202
1243
  text: z25.string().optional(),
1203
1244
  abstract: z25.string().optional(),
1204
1245
  encodingFormat: z25.string().optional(),
@@ -1234,9 +1275,10 @@ var NewsArticleSchema = ArticleSchema.extend({
1234
1275
  });
1235
1276
  var BlogPostingSchema = ArticleSchema.extend({
1236
1277
  "@type": z26.literal("BlogPosting").default("BlogPosting"),
1237
- sharedContent: z26.lazy(
1238
- () => z26.object({ "@type": z26.string() }).passthrough()
1239
- ).optional()
1278
+ sharedContent: z26.union([
1279
+ z26.object({ "@id": z26.string() }),
1280
+ z26.lazy(() => z26.object({ "@type": z26.string() }).catchall(z26.unknown()))
1281
+ ]).optional()
1240
1282
  });
1241
1283
  var createArticle = makeFactory(ArticleSchema);
1242
1284
  var createNewsArticle = makeFactory(NewsArticleSchema);
@@ -1246,9 +1288,12 @@ var createBlogPosting = makeFactory(BlogPostingSchema);
1246
1288
  import { z as z27 } from "zod";
1247
1289
  var WebPageSchema = CreativeWorkSchema.extend({
1248
1290
  "@type": z27.literal("WebPage").default("WebPage"),
1249
- breadcrumb: z27.lazy(
1250
- () => z27.object({ "@type": z27.literal("BreadcrumbList") }).catchall(z27.unknown())
1251
- ).optional(),
1291
+ breadcrumb: z27.union([
1292
+ z27.object({ "@id": z27.string() }),
1293
+ z27.lazy(
1294
+ () => z27.object({ "@type": z27.literal("BreadcrumbList") }).catchall(z27.unknown())
1295
+ )
1296
+ ]).optional(),
1252
1297
  lastReviewed: z27.string().optional(),
1253
1298
  reviewedBy: PersonOrOrgRef.optional(),
1254
1299
  speakable: z27.object({
@@ -1257,8 +1302,14 @@ var WebPageSchema = CreativeWorkSchema.extend({
1257
1302
  xpath: z27.union([z27.string(), z27.array(z27.string())]).optional()
1258
1303
  }).optional(),
1259
1304
  significantLink: z27.union([z27.url(), z27.array(z27.url())]).optional(),
1260
- mainContentOfPage: z27.object({ "@type": z27.string() }).catchall(z27.unknown()).optional(),
1261
- primaryImageOfPage: z27.object({ "@type": z27.string() }).catchall(z27.unknown()).optional(),
1305
+ mainContentOfPage: z27.union([
1306
+ z27.object({ "@id": z27.string() }),
1307
+ z27.object({ "@type": z27.string() }).catchall(z27.unknown())
1308
+ ]).optional(),
1309
+ primaryImageOfPage: z27.union([
1310
+ z27.object({ "@id": z27.string() }),
1311
+ z27.object({ "@type": z27.string() }).catchall(z27.unknown())
1312
+ ]).optional(),
1262
1313
  relatedLink: z27.union([z27.url(), z27.array(z27.url())]).optional()
1263
1314
  });
1264
1315
  var createWebPage = makeFactory(WebPageSchema);
@@ -1293,8 +1344,11 @@ var WebSiteSchema = CreativeWorkSchema.extend({
1293
1344
  publisher: PersonOrOrgRef.optional(),
1294
1345
  // Site-level metadata
1295
1346
  copyrightYear: z28.number().int().optional(),
1296
- // isPartOf — rarely used for WebSite itself but allowed
1297
- isPartOf: z28.lazy(() => z28.object({ "@type": z28.string() }).catchall(z28.unknown())).optional()
1347
+ // isPartOf — rarely used for WebSite itself but allowed (accept @id refs for @graph)
1348
+ isPartOf: z28.union([
1349
+ z28.object({ "@id": z28.string() }),
1350
+ z28.lazy(() => z28.object({ "@type": z28.string() }).catchall(z28.unknown()))
1351
+ ]).optional()
1298
1352
  });
1299
1353
  var createWebSite = makeFactory(WebSiteSchema);
1300
1354
 
@@ -1702,7 +1756,7 @@ var JobPostingSchema = z37.object({
1702
1756
  // Required by Google:
1703
1757
  title: z37.string(),
1704
1758
  description: z37.string(),
1705
- hiringOrganization: HiringOrgRef,
1759
+ hiringOrganization: z37.union([z37.object({ "@id": z37.string() }), HiringOrgRef]),
1706
1760
  jobLocation: z37.union([JobLocationRef, z37.array(JobLocationRef)]).optional(),
1707
1761
  datePosted: z37.string(),
1708
1762
  // ISO 8601 date
@@ -1816,7 +1870,10 @@ var QuizSchema = CreativeWorkSchema.extend({
1816
1870
  "@type": z38.literal("Quiz").default("Quiz"),
1817
1871
  name: z38.string().optional(),
1818
1872
  educationalLevel: z38.string().optional(),
1819
- about: z38.lazy(() => z38.object({ "@type": z38.string() }).catchall(z38.unknown())).optional(),
1873
+ about: z38.union([
1874
+ z38.object({ "@id": z38.string() }),
1875
+ z38.lazy(() => z38.object({ "@type": z38.string() }).catchall(z38.unknown()))
1876
+ ]).optional(),
1820
1877
  hasPart: z38.array(QuestionSchema).optional(),
1821
1878
  educationalAlignment: z38.union([AlignmentObjectSchema, z38.array(AlignmentObjectSchema)]).optional()
1822
1879
  });
@@ -1856,9 +1913,12 @@ var DiscussionForumPostingSchema = CreativeWorkSchema.extend({
1856
1913
  image: ImageOrUrl.optional(),
1857
1914
  /** "published", "draft", "deleted" — or a schema.org enum URL */
1858
1915
  creativeWorkStatus: z39.string().optional(),
1859
- mainEntityOfPage: z39.union([z39.url(), z39.object({}).catchall(z39.unknown())]).optional(),
1860
- // For reposts:
1861
- sharedContent: z39.lazy(() => z39.object({ "@type": z39.string() }).catchall(z39.unknown())).optional(),
1916
+ mainEntityOfPage: z39.union([z39.url(), z39.object({ "@id": z39.string() }), z39.object({}).catchall(z39.unknown())]).optional(),
1917
+ // For reposts (accept @id refs for @graph cross-referencing):
1918
+ sharedContent: z39.union([
1919
+ z39.object({ "@id": z39.string() }),
1920
+ z39.lazy(() => z39.object({ "@type": z39.string() }).catchall(z39.unknown()))
1921
+ ]).optional(),
1862
1922
  interactionStatistic: z39.union([InteractionCounterSchema, z39.array(InteractionCounterSchema)]).optional()
1863
1923
  });
1864
1924
  var createDiscussionForumPosting = makeFactory(
@@ -1895,13 +1955,17 @@ var createItemList = makeFactory(ItemListSchema);
1895
1955
  import { z as z41 } from "zod";
1896
1956
  var ProfilePageSchema = WebPageSchema.extend({
1897
1957
  "@type": z41.literal("ProfilePage").default("ProfilePage"),
1898
- // mainEntity: the person or org this profile is about (loose ref to avoid circular imports)
1899
- mainEntity: z41.lazy(
1900
- () => z41.object({ "@type": z41.string() }).catchall(z41.unknown())
1901
- ).optional(),
1958
+ // mainEntity: the person or org this profile is about (accept @id refs for @graph)
1959
+ mainEntity: z41.union([
1960
+ z41.object({ "@id": z41.string() }),
1961
+ z41.lazy(() => z41.object({ "@type": z41.string() }).catchall(z41.unknown()))
1962
+ ]).optional(),
1902
1963
  // hasPart: content published by/about this profile subject
1903
1964
  hasPart: z41.array(
1904
- z41.lazy(() => z41.object({ "@type": z41.string() }).catchall(z41.unknown()))
1965
+ z41.union([
1966
+ z41.object({ "@id": z41.string() }),
1967
+ z41.lazy(() => z41.object({ "@type": z41.string() }).catchall(z41.unknown()))
1968
+ ])
1905
1969
  ).optional(),
1906
1970
  dateCreated: z41.string().optional(),
1907
1971
  dateModified: z41.string().optional()
@@ -2116,6 +2180,104 @@ function createGraph(nodes) {
2116
2180
  return graph;
2117
2181
  }
2118
2182
 
2183
+ // src/core/ids.ts
2184
+ var SchemaIds = class {
2185
+ constructor(origin) {
2186
+ this.origin = origin;
2187
+ }
2188
+ /** @internal Build an ID from origin + fragment */
2189
+ id(fragment) {
2190
+ return `${this.origin}/#${fragment}`;
2191
+ }
2192
+ // ── Well-known entity IDs ───────────────────────────────────────────────
2193
+ // Things
2194
+ organization() {
2195
+ return this.id("organization");
2196
+ }
2197
+ localBusiness() {
2198
+ return this.id("localbusiness");
2199
+ }
2200
+ person() {
2201
+ return this.id("person");
2202
+ }
2203
+ product() {
2204
+ return this.id("product");
2205
+ }
2206
+ event() {
2207
+ return this.id("event");
2208
+ }
2209
+ place() {
2210
+ return this.id("place");
2211
+ }
2212
+ movie() {
2213
+ return this.id("movie");
2214
+ }
2215
+ // Creative works
2216
+ website() {
2217
+ return this.id("website");
2218
+ }
2219
+ webpage() {
2220
+ return this.id("webpage");
2221
+ }
2222
+ article() {
2223
+ return this.id("article");
2224
+ }
2225
+ breadcrumb() {
2226
+ return this.id("breadcrumb");
2227
+ }
2228
+ dataset() {
2229
+ return this.id("dataset");
2230
+ }
2231
+ recipe() {
2232
+ return this.id("recipe");
2233
+ }
2234
+ course() {
2235
+ return this.id("course");
2236
+ }
2237
+ softwareApplication() {
2238
+ return this.id("softwareapplication");
2239
+ }
2240
+ // Intangibles & other
2241
+ faqPage() {
2242
+ return this.id("faqpage");
2243
+ }
2244
+ jobPosting() {
2245
+ return this.id("jobposting");
2246
+ }
2247
+ vacationRental() {
2248
+ return this.id("vacationrental");
2249
+ }
2250
+ profilePage() {
2251
+ return this.id("profilepage");
2252
+ }
2253
+ // ── Custom IDs ──────────────────────────────────────────────────────────
2254
+ /**
2255
+ * Generate an ID with any custom fragment.
2256
+ * @example ids.custom("hero-banner") // "https://example.com/#hero-banner"
2257
+ */
2258
+ custom(fragment) {
2259
+ return this.id(fragment);
2260
+ }
2261
+ /**
2262
+ * Generate an ID scoped to a specific page path.
2263
+ * @example ids.forPath("/about", "webpage") // "https://example.com/about#webpage"
2264
+ */
2265
+ forPath(path, fragment) {
2266
+ return `${this.origin}${path}#${fragment}`;
2267
+ }
2268
+ // ── Reference helper ────────────────────────────────────────────────────
2269
+ /**
2270
+ * Returns a `{ "@id": "..." }` reference object for cross-referencing
2271
+ * entities within a @graph. Pass to fields like publisher, organizer, etc.
2272
+ *
2273
+ * @example
2274
+ * createWebSite({ publisher: ids.ref("organization"), ... })
2275
+ */
2276
+ ref(fragment) {
2277
+ return { "@id": this.id(fragment) };
2278
+ }
2279
+ };
2280
+
2119
2281
  // src/helpers/breadcrumb.ts
2120
2282
  import { z as z43 } from "zod";
2121
2283
  var BreadcrumbListSchema = z43.object({
@@ -2279,6 +2441,7 @@ export {
2279
2441
  RecipeSchema,
2280
2442
  ReviewSchema,
2281
2443
  SchemaGraph,
2444
+ SchemaIds,
2282
2445
  SchemaNode,
2283
2446
  SeekToActionSchema,
2284
2447
  ServicePeriodSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schemaorg-kit",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Type-safe schema.org structured data builder — all Google rich result types, Zod validation, JSON-LD @graph",
5
5
  "keywords": [
6
6
  "schema.org",