@cms0/cms0 0.2.6 → 0.2.8

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.
@@ -1,3 +1,4 @@
1
+ import { computeUnionBranchKeys, } from "@cms0/shared";
1
2
  const primitive = (type) => ({
2
3
  kind: "primitive",
3
4
  type,
@@ -49,6 +50,237 @@ const localizedRichTextDescriptor = {
49
50
  type: "json",
50
51
  customType: "LocalizedRichText",
51
52
  };
53
+ function createUnionDescriptor(branches) {
54
+ return {
55
+ kind: "union",
56
+ anyOf: branches,
57
+ branchKeys: computeUnionBranchKeys(branches),
58
+ };
59
+ }
60
+ const localizedOrStringDescriptor = createUnionDescriptor([
61
+ localizedStringDescriptor,
62
+ {
63
+ kind: "primitive",
64
+ type: "string",
65
+ },
66
+ ]);
67
+ const keywordsDescriptor = createUnionDescriptor([
68
+ localizedStringDescriptor,
69
+ {
70
+ type: "array",
71
+ items: {
72
+ kind: "primitive",
73
+ type: "string",
74
+ },
75
+ },
76
+ ]);
77
+ const robotsDescriptor = createUnionDescriptor([
78
+ {
79
+ kind: "primitive",
80
+ type: "boolean",
81
+ },
82
+ {
83
+ type: "object",
84
+ properties: {
85
+ index: {
86
+ kind: "primitive",
87
+ type: "boolean",
88
+ optional: true,
89
+ nullable: false,
90
+ },
91
+ follow: {
92
+ kind: "primitive",
93
+ type: "boolean",
94
+ optional: true,
95
+ nullable: false,
96
+ },
97
+ nocache: {
98
+ kind: "primitive",
99
+ type: "boolean",
100
+ optional: true,
101
+ nullable: false,
102
+ },
103
+ },
104
+ },
105
+ ]);
106
+ const alternatesDescriptor = {
107
+ type: "object",
108
+ properties: {
109
+ canonical: {
110
+ kind: "primitive",
111
+ type: "string",
112
+ optional: true,
113
+ nullable: false,
114
+ },
115
+ languages: {
116
+ kind: "primitive",
117
+ type: "json",
118
+ optional: true,
119
+ nullable: false,
120
+ },
121
+ },
122
+ };
123
+ const imageOrStringDescriptor = createUnionDescriptor([
124
+ {
125
+ kind: "modelRef",
126
+ model: "Image",
127
+ },
128
+ {
129
+ kind: "primitive",
130
+ type: "string",
131
+ },
132
+ ]);
133
+ const openGraphDescriptor = {
134
+ type: "object",
135
+ properties: {
136
+ type: {
137
+ kind: "enum",
138
+ valueType: "string",
139
+ values: ["website", "article", "profile"],
140
+ optional: true,
141
+ nullable: false,
142
+ },
143
+ url: {
144
+ kind: "primitive",
145
+ type: "string",
146
+ optional: true,
147
+ nullable: false,
148
+ },
149
+ siteName: {
150
+ kind: "primitive",
151
+ type: "string",
152
+ optional: true,
153
+ nullable: false,
154
+ },
155
+ title: {
156
+ ...localizedOrStringDescriptor,
157
+ optional: true,
158
+ nullable: false,
159
+ },
160
+ description: {
161
+ ...localizedOrStringDescriptor,
162
+ optional: true,
163
+ nullable: false,
164
+ },
165
+ images: {
166
+ type: "array",
167
+ items: imageOrStringDescriptor,
168
+ optional: true,
169
+ nullable: false,
170
+ },
171
+ locale: {
172
+ kind: "primitive",
173
+ type: "string",
174
+ optional: true,
175
+ nullable: false,
176
+ },
177
+ alternateLocale: {
178
+ type: "array",
179
+ items: {
180
+ kind: "primitive",
181
+ type: "string",
182
+ },
183
+ optional: true,
184
+ nullable: false,
185
+ },
186
+ },
187
+ };
188
+ const twitterDescriptor = {
189
+ type: "object",
190
+ properties: {
191
+ card: {
192
+ kind: "enum",
193
+ valueType: "string",
194
+ values: ["summary", "summary_large_image", "app", "player"],
195
+ optional: true,
196
+ nullable: false,
197
+ },
198
+ site: {
199
+ kind: "primitive",
200
+ type: "string",
201
+ optional: true,
202
+ nullable: false,
203
+ },
204
+ creator: {
205
+ kind: "primitive",
206
+ type: "string",
207
+ optional: true,
208
+ nullable: false,
209
+ },
210
+ title: {
211
+ ...localizedOrStringDescriptor,
212
+ optional: true,
213
+ nullable: false,
214
+ },
215
+ description: {
216
+ ...localizedOrStringDescriptor,
217
+ optional: true,
218
+ nullable: false,
219
+ },
220
+ images: {
221
+ type: "array",
222
+ items: imageOrStringDescriptor,
223
+ optional: true,
224
+ nullable: false,
225
+ },
226
+ },
227
+ };
228
+ const seoDescriptor = {
229
+ type: "object",
230
+ customType: "Seo",
231
+ properties: {
232
+ title: {
233
+ ...localizedOrStringDescriptor,
234
+ optional: true,
235
+ nullable: false,
236
+ },
237
+ description: {
238
+ ...localizedOrStringDescriptor,
239
+ optional: true,
240
+ nullable: false,
241
+ },
242
+ keywords: {
243
+ ...keywordsDescriptor,
244
+ optional: true,
245
+ nullable: false,
246
+ },
247
+ canonical: {
248
+ kind: "primitive",
249
+ type: "string",
250
+ optional: true,
251
+ nullable: false,
252
+ },
253
+ robots: {
254
+ ...robotsDescriptor,
255
+ optional: true,
256
+ nullable: false,
257
+ },
258
+ alternates: {
259
+ ...alternatesDescriptor,
260
+ optional: true,
261
+ nullable: false,
262
+ },
263
+ openGraph: {
264
+ ...openGraphDescriptor,
265
+ optional: true,
266
+ nullable: false,
267
+ },
268
+ twitter: {
269
+ ...twitterDescriptor,
270
+ optional: true,
271
+ nullable: false,
272
+ },
273
+ jsonLd: {
274
+ type: "array",
275
+ items: {
276
+ kind: "primitive",
277
+ type: "json",
278
+ },
279
+ optional: true,
280
+ nullable: false,
281
+ },
282
+ },
283
+ };
52
284
  export const customModelDescriptors = {
53
285
  File: fileDescriptor,
54
286
  Image: imageDescriptor,
@@ -58,6 +290,7 @@ export const customInlineDescriptors = {
58
290
  RichText: richTextDescriptor,
59
291
  LocalizedString: localizedStringDescriptor,
60
292
  LocalizedRichText: localizedRichTextDescriptor,
293
+ Seo: seoDescriptor,
61
294
  };
62
295
  export const customInlineTypeMetadata = {
63
296
  RichText: {},
@@ -67,6 +300,7 @@ export const customInlineTypeMetadata = {
67
300
  LocalizedRichText: {
68
301
  includeId: { mapLikeFields: ["locales"] },
69
302
  },
303
+ Seo: {},
70
304
  };
71
305
  export const customTypeDescriptors = {
72
306
  ...customModelDescriptors,
@@ -85,13 +319,20 @@ export function resolveCustomTypeDependencies(names) {
85
319
  const name = queue.shift();
86
320
  if (!name || resolved.has(name))
87
321
  continue;
88
- const descriptor = customModelDescriptors[name];
322
+ const modelDescriptor = customModelDescriptors[name];
323
+ const inlineDescriptor = customInlineDescriptors[name];
324
+ const descriptor = modelDescriptor ?? inlineDescriptor;
89
325
  if (!descriptor)
90
326
  continue;
91
327
  resolved.add(name);
92
328
  const refs = new Set();
93
- for (const prop of Object.values(descriptor.properties)) {
94
- collectModelRefs(prop, refs);
329
+ if (modelDescriptor) {
330
+ for (const prop of Object.values(modelDescriptor.properties)) {
331
+ collectModelRefs(prop, refs);
332
+ }
333
+ }
334
+ else {
335
+ collectModelRefs(inlineDescriptor, refs);
95
336
  }
96
337
  refs.forEach((ref) => {
97
338
  if (customTypeDescriptors[ref] && !resolved.has(ref)) {
@@ -106,11 +347,15 @@ function collectModelRefs(desc, out) {
106
347
  out.add(desc.model);
107
348
  return;
108
349
  }
109
- if (desc.type === "array") {
350
+ if ("type" in desc && desc.type === "array") {
110
351
  collectModelRefs(desc.items, out);
111
352
  return;
112
353
  }
113
- if (desc.type === "object") {
354
+ if ("type" in desc && desc.type === "object") {
114
355
  Object.values(desc.properties).forEach((child) => collectModelRefs(child, out));
356
+ return;
357
+ }
358
+ if (desc.kind === "union") {
359
+ desc.anyOf.forEach((branch) => collectModelRefs(branch, out));
115
360
  }
116
361
  }
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { schemaDescriptor } from "@cms0/cms0/schema-descriptors";
2
- import { buildZodSchemasFromDescriptor, } from "@cms0/shared";
2
+ import { buildZodSchemasFromDescriptor, decodeTaggedUnionValue, getUnionBranchKeys, } from "@cms0/shared";
3
3
  import { getCustomInlineTypeMetadata } from "./custom-types/registry.js";
4
4
  const DEFAULT_MODEL_NORMALIZATION_CONCURRENCY = 8;
5
5
  const COLLECTION_SHAPE_ERROR = "Invalid collection response. Expected array or { items, total }.";
@@ -105,6 +105,9 @@ function snakeCaseModelIdKey(modelName) {
105
105
  .toLowerCase();
106
106
  return `${snake}_id`;
107
107
  }
108
+ function isUuidLikeId(value) {
109
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
110
+ }
108
111
  function extractModelRefId(raw, modelName, options) {
109
112
  if (typeof raw === "string")
110
113
  return raw;
@@ -145,6 +148,15 @@ function extractModelRefId(raw, modelName, options) {
145
148
  function isPrimitiveDescriptor(desc) {
146
149
  return desc.kind === "primitive";
147
150
  }
151
+ function isEnumDescriptor(desc) {
152
+ return desc.kind === "enum" && Array.isArray(desc.values);
153
+ }
154
+ function isUnionDescriptor(desc) {
155
+ return desc.kind === "union" && Array.isArray(desc.anyOf);
156
+ }
157
+ function isScalarDescriptor(desc) {
158
+ return isPrimitiveDescriptor(desc) || isEnumDescriptor(desc) || isUnionDescriptor(desc);
159
+ }
148
160
  function isModelRefDescriptor(desc) {
149
161
  return desc.kind === "modelRef";
150
162
  }
@@ -162,7 +174,7 @@ function attachIdIfObject(value, id) {
162
174
  }
163
175
  return value;
164
176
  }
165
- function attachIdForPrimitiveDescriptor(descriptor, value, id) {
177
+ function attachIdForScalarDescriptor(descriptor, value, id) {
166
178
  const withId = attachIdIfObject(value, id);
167
179
  if (!withId || typeof withId !== "object" || Array.isArray(withId)) {
168
180
  return withId;
@@ -358,6 +370,23 @@ function coerceLocalizedRichTextValue(value, locale, defaultLocale) {
358
370
  }
359
371
  return projectLocalizedByLocale(normalized, locale);
360
372
  }
373
+ function coerceSeoValue(value) {
374
+ if (value == null)
375
+ return value;
376
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
377
+ return {};
378
+ }
379
+ const source = value;
380
+ const keys = Object.keys(source).filter((key) => key !== "id");
381
+ if (keys.length === 1 &&
382
+ keys[0] === "value" &&
383
+ source.value &&
384
+ typeof source.value === "object" &&
385
+ !Array.isArray(source.value)) {
386
+ return source.value;
387
+ }
388
+ return source;
389
+ }
361
390
  function coerceCustomTypeValue(descriptor, value, options) {
362
391
  const customType = descriptor.customType;
363
392
  if (!customType)
@@ -373,8 +402,81 @@ function coerceCustomTypeValue(descriptor, value, options) {
373
402
  if (customType === "LocalizedRichText") {
374
403
  return coerceLocalizedRichTextValue(value, options?.locale, options?.defaultLocale);
375
404
  }
405
+ if (customType === "Seo") {
406
+ return coerceSeoValue(value);
407
+ }
376
408
  return value;
377
409
  }
410
+ function coercePrimitiveValueByType(type, value) {
411
+ if (type === "string") {
412
+ if (value == null)
413
+ return "";
414
+ return typeof value === "string" ? value : String(value);
415
+ }
416
+ if (type === "number") {
417
+ if (typeof value === "number")
418
+ return Number.isFinite(value) ? value : null;
419
+ const parsed = Number(value);
420
+ return Number.isFinite(parsed) ? parsed : null;
421
+ }
422
+ if (type === "boolean") {
423
+ if (typeof value === "boolean")
424
+ return value;
425
+ if (value === "true" || value === "1" || value === 1)
426
+ return true;
427
+ if (value === "false" || value === "0" || value === 0)
428
+ return false;
429
+ return Boolean(value);
430
+ }
431
+ return value;
432
+ }
433
+ function coerceEnumValue(descriptor, value) {
434
+ const valueType = (descriptor.valueType ?? "string");
435
+ const coerced = coercePrimitiveValueByType(valueType, value);
436
+ const values = Array.isArray(descriptor.values)
437
+ ? descriptor.values
438
+ : [];
439
+ if (!values.length)
440
+ return coerced;
441
+ if (values.some((entry) => Object.is(entry, coerced))) {
442
+ return coerced;
443
+ }
444
+ return values[0];
445
+ }
446
+ function decodeUnionEnvelope(descriptor, raw) {
447
+ const decoded = decodeTaggedUnionValue(raw);
448
+ if (!decoded)
449
+ return null;
450
+ const branches = Array.isArray(descriptor.anyOf)
451
+ ? descriptor.anyOf
452
+ : [];
453
+ if (!branches.length)
454
+ return null;
455
+ const branchKeys = getUnionBranchKeys(descriptor);
456
+ const branchIndex = branchKeys.findIndex((key) => key === decoded.branchKey);
457
+ if (branchIndex < 0 || branchIndex >= branches.length)
458
+ return null;
459
+ const branchDescriptor = branches[branchIndex];
460
+ if (!branchDescriptor)
461
+ return null;
462
+ return {
463
+ branchDescriptor,
464
+ branchValue: decoded.value,
465
+ };
466
+ }
467
+ function decodeUnionValueFromRawField(descriptor, raw) {
468
+ const direct = decodeUnionEnvelope(descriptor, raw);
469
+ if (direct) {
470
+ return { ...direct, rowId: extractId(raw) };
471
+ }
472
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && "value" in raw) {
473
+ const nested = decodeUnionEnvelope(descriptor, raw.value);
474
+ if (nested) {
475
+ return { ...nested, rowId: extractId(raw) };
476
+ }
477
+ }
478
+ return null;
479
+ }
378
480
  function ensureCollectionEnvelope(data, path) {
379
481
  if (Array.isArray(data)) {
380
482
  return { items: data, total: data.length };
@@ -446,12 +548,8 @@ async function normalizeObjectField(descriptor, path, raw, context, trail, isCol
446
548
  const source = raw && typeof raw === "object" ? raw : {};
447
549
  const output = {};
448
550
  for (const [propertyName, propertyDescriptor] of Object.entries(descriptor.properties)) {
449
- if (isPrimitiveDescriptor(propertyDescriptor)) {
450
- output[propertyName] = coerceCustomTypeValue(propertyDescriptor, source[propertyName], {
451
- locale: context.options.locale,
452
- defaultLocale: context.options.defaultLocale,
453
- assetUrlBuilder: context.options.assetUrlBuilder,
454
- });
551
+ if (isScalarDescriptor(propertyDescriptor)) {
552
+ output[propertyName] = await normalizeField(propertyDescriptor, `${path}/${propertyName}`, source[propertyName], context, trail, false);
455
553
  continue;
456
554
  }
457
555
  if (isModelRefDescriptor(propertyDescriptor)) {
@@ -539,16 +637,14 @@ async function normalizeObjectField(descriptor, path, raw, context, trail, isCol
539
637
  async function normalizeArrayField(descriptor, path, raw, context, trail) {
540
638
  const envelope = ensureCollectionEnvelope(raw, path);
541
639
  const itemDescriptor = descriptor.items;
542
- if (isPrimitiveDescriptor(itemDescriptor)) {
543
- return envelope.items.map((row) => {
544
- const value = row && typeof row === "object" && "value" in row
545
- ? row.value
546
- : row;
640
+ if (isScalarDescriptor(itemDescriptor)) {
641
+ return mapWithConcurrency(envelope.items, context.options.modelNormalizationConcurrency, async (row) => {
642
+ const normalizedValue = await normalizeField(itemDescriptor, path, row, context, trail, true);
547
643
  if (context.options.includeIdMode !== "all") {
548
- return value;
644
+ return normalizedValue;
549
645
  }
550
646
  const id = extractId(row);
551
- const valueWithId = attachIdForPrimitiveDescriptor(itemDescriptor, value, id);
647
+ const valueWithId = attachIdForScalarDescriptor(itemDescriptor, normalizedValue, id);
552
648
  const isObjectValue = valueWithId && typeof valueWithId === "object" && !Array.isArray(valueWithId);
553
649
  if (!id) {
554
650
  return valueWithId;
@@ -579,6 +675,60 @@ async function normalizeArrayField(descriptor, path, raw, context, trail) {
579
675
  }
580
676
  return envelope.items;
581
677
  }
678
+ async function normalizeInlineField(descriptor, raw, context, trail, isCollectionItem) {
679
+ if (isPrimitiveDescriptor(descriptor)) {
680
+ return coerceCustomTypeValue(descriptor, raw, {
681
+ locale: context.options.locale,
682
+ defaultLocale: context.options.defaultLocale,
683
+ assetUrlBuilder: context.options.assetUrlBuilder,
684
+ });
685
+ }
686
+ if (isEnumDescriptor(descriptor)) {
687
+ return coerceEnumValue(descriptor, raw);
688
+ }
689
+ if (isUnionDescriptor(descriptor)) {
690
+ if (raw == null)
691
+ return raw;
692
+ const decoded = decodeUnionEnvelope(descriptor, raw);
693
+ if (!decoded) {
694
+ return null;
695
+ }
696
+ return normalizeInlineField(decoded.branchDescriptor, decoded.branchValue, context, trail, isCollectionItem);
697
+ }
698
+ if (isModelRefDescriptor(descriptor)) {
699
+ const refId = extractModelRefId(raw, descriptor.model, {
700
+ allowObjectIdFallback: true,
701
+ });
702
+ if (!refId) {
703
+ return missingModelRefValue(descriptor);
704
+ }
705
+ return normalizeModelRef(descriptor.model, refId, context, trail, isCollectionItem, raw);
706
+ }
707
+ if (isArrayDescriptor(descriptor)) {
708
+ const source = Array.isArray(raw) ? raw : [];
709
+ const normalized = await mapWithConcurrency(source, context.options.modelNormalizationConcurrency, async (entry) => normalizeInlineField(descriptor.items, entry, context, trail, true));
710
+ return coerceCustomTypeValue(descriptor, normalized, {
711
+ locale: context.options.locale,
712
+ defaultLocale: context.options.defaultLocale,
713
+ assetUrlBuilder: context.options.assetUrlBuilder,
714
+ });
715
+ }
716
+ if (isObjectDescriptor(descriptor)) {
717
+ const source = raw && typeof raw === "object" && !Array.isArray(raw)
718
+ ? raw
719
+ : {};
720
+ const output = {};
721
+ for (const [propertyName, propertyDescriptor] of Object.entries(descriptor.properties)) {
722
+ output[propertyName] = await normalizeInlineField(propertyDescriptor, source[propertyName], context, trail, false);
723
+ }
724
+ return coerceCustomTypeValue(descriptor, output, {
725
+ locale: context.options.locale,
726
+ defaultLocale: context.options.defaultLocale,
727
+ assetUrlBuilder: context.options.assetUrlBuilder,
728
+ });
729
+ }
730
+ return raw;
731
+ }
582
732
  async function normalizeField(descriptor, path, raw, context, trail, isCollectionItem) {
583
733
  if (isPrimitiveDescriptor(descriptor)) {
584
734
  const value = raw && typeof raw === "object" && "value" in raw
@@ -591,10 +741,33 @@ async function normalizeField(descriptor, path, raw, context, trail, isCollectio
591
741
  });
592
742
  if (shouldIncludeObjectId(context.options.includeIdMode, isCollectionItem)) {
593
743
  const id = extractId(raw);
594
- coerced = attachIdForPrimitiveDescriptor(descriptor, coerced, id);
744
+ coerced = attachIdForScalarDescriptor(descriptor, coerced, id);
595
745
  }
596
746
  return coerced;
597
747
  }
748
+ if (isEnumDescriptor(descriptor)) {
749
+ const value = raw && typeof raw === "object" && "value" in raw
750
+ ? raw.value
751
+ : raw;
752
+ let coerced = coerceEnumValue(descriptor, value);
753
+ if (shouldIncludeObjectId(context.options.includeIdMode, isCollectionItem)) {
754
+ const id = extractId(raw);
755
+ coerced = attachIdForScalarDescriptor(descriptor, coerced, id);
756
+ }
757
+ return coerced;
758
+ }
759
+ if (isUnionDescriptor(descriptor)) {
760
+ const decoded = decodeUnionValueFromRawField(descriptor, raw);
761
+ if (!decoded) {
762
+ return null;
763
+ }
764
+ let normalized = await normalizeInlineField(decoded.branchDescriptor, decoded.branchValue, context, trail, isCollectionItem);
765
+ if (shouldIncludeObjectId(context.options.includeIdMode, isCollectionItem)) {
766
+ const id = decoded.rowId ?? extractId(raw);
767
+ normalized = attachIdForScalarDescriptor(descriptor, normalized, id);
768
+ }
769
+ return normalized;
770
+ }
598
771
  if (isModelRefDescriptor(descriptor)) {
599
772
  const inlineModelDescriptor = context.modelDescriptors.get(descriptor.model);
600
773
  const inlineModel = inlineModelDescriptor
@@ -881,14 +1054,25 @@ export function createCmsClient(descriptor, config) {
881
1054
  return buildModelAccessor(modelEntry);
882
1055
  },
883
1056
  });
884
- const rootProxy = new Proxy({}, {
885
- get(_target, property) {
886
- if (typeof property !== "string")
887
- return undefined;
1057
+ const clientMeta = {
1058
+ locales: config.locales ?? [],
1059
+ defaultLocale: config.defaultLocale,
1060
+ includeIdDefault: !!config.includeId,
1061
+ };
1062
+ const rootProxyTarget = {
1063
+ models: modelsProxy,
1064
+ meta: clientMeta,
1065
+ };
1066
+ const rootProxy = new Proxy(rootProxyTarget, {
1067
+ get(target, property, receiver) {
1068
+ if (typeof property !== "string") {
1069
+ return Reflect.get(target, property, receiver);
1070
+ }
888
1071
  if (property === "then")
889
1072
  return undefined;
890
- if (property === "models")
891
- return modelsProxy;
1073
+ if (Reflect.has(target, property)) {
1074
+ return Reflect.get(target, property, receiver);
1075
+ }
892
1076
  const entry = registry.roots.get(property) ??
893
1077
  registry.rootsCaseInsensitive.get(property.toLowerCase());
894
1078
  if (!entry) {
@@ -897,14 +1081,9 @@ export function createCmsClient(descriptor, config) {
897
1081
  return async (options) => runResource(entry, descriptor, baseUrl, apiKey, config.defaultLocale, assetUrlBuilder, zodSchemas, modelZodSchemas, globalIncludeId, sharedModelInflightCache, options);
898
1082
  },
899
1083
  });
900
- const client = Object.assign(rootProxy, {
901
- models: modelsProxy,
902
- locales: config.locales ?? [],
903
- defaultLocale: config.defaultLocale,
904
- includeIdDefault: !!config.includeId,
905
- });
906
- return client;
1084
+ return rootProxy;
907
1085
  }
908
1086
  export function cms0(config) {
909
1087
  return createCmsClient(schemaDescriptor, config);
910
1088
  }
1089
+ export { resolveLocalized, toNextMetadata, toOpenGraph, toTwitter, } from "./seo.js";