canopycms 0.4.0 → 0.5.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +66 -42
  2. package/dist/ai/generate.d.ts +3 -0
  3. package/dist/ai/generate.js +12 -29
  4. package/dist/ai/handler.js +1 -0
  5. package/dist/ai/types.d.ts +3 -9
  6. package/dist/api/reference-options.d.ts +2 -5
  7. package/dist/api/reference-options.js +21 -2
  8. package/dist/api/schema.d.ts +4 -4
  9. package/dist/build/generate-ai-content.js +1 -0
  10. package/dist/cli/cli.js +131 -38
  11. package/dist/cli/generate-ai-content.js +75 -32
  12. package/dist/cli/init.js +29 -4
  13. package/dist/config/index.d.ts +1 -1
  14. package/dist/config/schemas/config.d.ts +74 -0
  15. package/dist/config/schemas/config.js +21 -0
  16. package/dist/config/schemas/field.d.ts +30 -0
  17. package/dist/config/schemas/field.js +22 -0
  18. package/dist/config/types.d.ts +39 -0
  19. package/dist/config/validation.d.ts +6 -0
  20. package/dist/config/validation.js +33 -0
  21. package/dist/editor/FormRenderer.js +15 -13
  22. package/dist/editor/copy.d.ts +5 -0
  23. package/dist/editor/copy.js +5 -0
  24. package/dist/editor/fields/BlockField.d.ts +2 -0
  25. package/dist/editor/fields/BlockField.js +15 -8
  26. package/dist/editor/fields/ItemList.d.ts +21 -3
  27. package/dist/editor/fields/ItemList.js +16 -8
  28. package/dist/editor/fields/NumberListField.d.ts +2 -0
  29. package/dist/editor/fields/NumberListField.js +6 -2
  30. package/dist/editor/fields/ObjectListField.js +9 -3
  31. package/dist/editor/fields/ReferenceField.d.ts +6 -7
  32. package/dist/editor/fields/ReferenceField.js +28 -122
  33. package/dist/editor/fields/ReferenceListField.d.ts +41 -0
  34. package/dist/editor/fields/ReferenceListField.js +139 -0
  35. package/dist/editor/fields/SelectField.d.ts +2 -0
  36. package/dist/editor/fields/SelectField.js +6 -3
  37. package/dist/editor/fields/StringListField.d.ts +10 -1
  38. package/dist/editor/fields/StringListField.js +18 -8
  39. package/dist/editor/fields/item-expansion.d.ts +7 -3
  40. package/dist/editor/fields/item-expansion.js +6 -4
  41. package/dist/editor/fields/list-item-keys.d.ts +6 -5
  42. package/dist/editor/fields/list-item-keys.js +45 -13
  43. package/dist/editor/fields/reference-noindex.d.ts +37 -0
  44. package/dist/editor/fields/reference-noindex.js +48 -0
  45. package/dist/editor/fields/use-reference-options.d.ts +36 -0
  46. package/dist/editor/fields/use-reference-options.js +121 -0
  47. package/dist/editor/form-outline.d.ts +2 -2
  48. package/dist/editor/form-outline.js +4 -2
  49. package/dist/editor/theme.js +5 -0
  50. package/dist/entry-schema-registry.d.ts +1 -1
  51. package/dist/entry-schema-registry.js +3 -2
  52. package/dist/entry-schema.d.ts +5 -3
  53. package/dist/entry-schema.js +2 -1
  54. package/dist/reference-resolver.d.ts +18 -1
  55. package/dist/reference-resolver.js +175 -8
  56. package/dist/schema/schema-store.d.ts +2 -2
  57. package/dist/static/index.d.ts +8 -4
  58. package/dist/static/index.js +38 -13
  59. package/dist/static/seo.d.ts +7 -5
  60. package/dist/static/seo.js +13 -6
  61. package/dist/validation/entry-validator.d.ts +5 -0
  62. package/dist/validation/entry-validator.js +12 -0
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +1 -1
@@ -363,14 +363,14 @@ export declare const updateEntryType: import("./route-builder.js").RouteDefiniti
363
363
  default?: boolean | undefined;
364
364
  format?: "mdx" | "md" | "json" | "yaml" | undefined;
365
365
  label?: string | undefined;
366
- schema?: string | undefined;
367
366
  maxItems?: number | undefined;
367
+ schema?: string | undefined;
368
368
  }, {
369
369
  default?: boolean | undefined;
370
370
  format?: "mdx" | "md" | "json" | "yaml" | undefined;
371
371
  label?: string | undefined;
372
- schema?: string | undefined;
373
372
  maxItems?: number | undefined;
373
+ schema?: string | undefined;
374
374
  }>, UpdateEntryTypeApiResponse>;
375
375
  /**
376
376
  * DELETE /:branch/schema/entry-types/:entryTypeName/...collectionPath - Remove entry type
@@ -640,14 +640,14 @@ export declare const SCHEMA_ROUTES: {
640
640
  default?: boolean | undefined;
641
641
  format?: "mdx" | "md" | "json" | "yaml" | undefined;
642
642
  label?: string | undefined;
643
- schema?: string | undefined;
644
643
  maxItems?: number | undefined;
644
+ schema?: string | undefined;
645
645
  }, {
646
646
  default?: boolean | undefined;
647
647
  format?: "mdx" | "md" | "json" | "yaml" | undefined;
648
648
  label?: string | undefined;
649
- schema?: string | undefined;
650
649
  maxItems?: number | undefined;
650
+ schema?: string | undefined;
651
651
  }>, UpdateEntryTypeApiResponse>;
652
652
  readonly removeEntryType: import("./route-builder.js").RouteDefinition<z.ZodObject<{
653
653
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
@@ -200,6 +200,7 @@ export async function generateAIContentFiles(options) {
200
200
  contentRoot: contentRootName,
201
201
  config: aiConfig,
202
202
  entryLinkUrl: config.entryLinkUrl,
203
+ seo: config.seo,
203
204
  ...resolveBuildStamp(),
204
205
  });
205
206
  const absoluteOutputDir = path.resolve(outputDir) + path.sep;
package/dist/cli/cli.js CHANGED
@@ -1223,7 +1223,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
1223
1223
  }
1224
1224
  console.log("\nDone");
1225
1225
  }
1226
- var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes, init_types, SAFE_HTML_TAGS, COMPONENT_NAME, MDX_BINDINGS, FORBIDDEN_ATTRIBUTES, init_mdx_allowlist, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, previewPrefixSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, PLACEHOLDER_GITHUB_REPO, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, envFlag, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, cms_task_queue_exports, debugLogger, cmsTaskQueueLogger, init_cms_task_queue, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
1226
+ var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes, init_types, SAFE_HTML_TAGS, COMPONENT_NAME, MDX_BINDINGS, FORBIDDEN_ATTRIBUTES, init_mdx_allowlist, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, previewPrefixSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, PLACEHOLDER_GITHUB_REPO, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, envFlag, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, cms_task_queue_exports, debugLogger, cmsTaskQueueLogger, init_cms_task_queue, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
1227
1227
  var init_init = __esm({
1228
1228
  "dist/cli/init.js"() {
1229
1229
  "use strict";
@@ -1361,7 +1361,10 @@ var init_init = __esm({
1361
1361
  required: z.boolean().optional(),
1362
1362
  list: z.boolean().optional(),
1363
1363
  isTitle: z.boolean().optional(),
1364
- isBody: z.boolean().optional()
1364
+ isBody: z.boolean().optional(),
1365
+ // Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
1366
+ // `ensureMaxItemsPlacement`'s call (config/validation.ts).
1367
+ maxItems: z.number().int().positive().optional()
1365
1368
  });
1366
1369
  selectOptionSchema = z.union([
1367
1370
  z.string(),
@@ -1419,6 +1422,7 @@ var init_init = __esm({
1419
1422
  type: z.literal("select"),
1420
1423
  options: z.array(selectOptionSchema).min(1)
1421
1424
  });
1425
+ referenceSortFieldSchema = z.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
1422
1426
  referenceFieldSchema = fieldBaseSchema.extend({
1423
1427
  type: z.literal("reference"),
1424
1428
  collections: z.array(z.string().min(1)).min(1).optional(),
@@ -1429,7 +1433,14 @@ var init_init = __esm({
1429
1433
  // default, so a consumer that adopts this schema's parse output would silently delete a
1430
1434
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
1431
1435
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
1432
- includeBody: z.boolean().optional()
1436
+ includeBody: z.boolean().optional(),
1437
+ optionSort: z.union([
1438
+ z.enum(["label", "collection"]),
1439
+ z.object({
1440
+ field: referenceSortFieldSchema,
1441
+ direction: z.enum(["asc", "desc"]).optional()
1442
+ }).strict()
1443
+ ]).optional()
1433
1444
  });
1434
1445
  fieldHolder = [z.never()];
1435
1446
  blockSchema = z.object({
@@ -1614,6 +1625,19 @@ var init_init = __esm({
1614
1625
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
1615
1626
  SignInComponent: z5.custom().optional()
1616
1627
  });
1628
+ seoFieldNameSchema = z5.string().min(1).optional();
1629
+ seoSchema = z5.object({
1630
+ group: z5.string().min(1).optional(),
1631
+ fields: z5.object({
1632
+ title: seoFieldNameSchema,
1633
+ description: seoFieldNameSchema,
1634
+ ogImage: seoFieldNameSchema,
1635
+ ogType: seoFieldNameSchema,
1636
+ canonical: seoFieldNameSchema,
1637
+ noindex: seoFieldNameSchema,
1638
+ twitterCard: seoFieldNameSchema
1639
+ }).strict().optional()
1640
+ }).strict();
1617
1641
  CanopyConfigSchema = z5.object({
1618
1642
  media: mediaSchema.optional(),
1619
1643
  // No outer .optional(): both schemas already .default('deny'), so the field is optional on
@@ -1663,6 +1687,7 @@ var init_init = __esm({
1663
1687
  entryLinkUrl: z5.custom().optional(),
1664
1688
  validateEntry: z5.custom().optional(),
1665
1689
  mdxAllow: mdxAllowlistSchema.optional(),
1690
+ seo: seoSchema.optional(),
1666
1691
  dev: devConfigSchema.optional()
1667
1692
  }).strict();
1668
1693
  DEFAULT_PROD_WORKSPACE = "/mnt/efs";
@@ -4569,7 +4594,11 @@ function linkOnlyReferenceData(data, format, fields) {
4569
4594
  return rest;
4570
4595
  }
4571
4596
  function extractSeoFields(entryData, opts = {}) {
4572
- const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
4597
+ const names = { ...DEFAULT_SEO_FIELD_NAMES };
4598
+ for (const [key, name] of Object.entries(opts.fields ?? {})) {
4599
+ if (typeof name === "string" && name !== "")
4600
+ names[key] = name;
4601
+ }
4573
4602
  const entry = asRecord2(entryData);
4574
4603
  const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
4575
4604
  return {
@@ -6254,7 +6283,7 @@ function createReferenceRendering(idIndex, flatSchema, entryLinkUrl, markdownCop
6254
6283
  };
6255
6284
  }
6256
6285
  async function generateAIContent(options) {
6257
- const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
6286
+ const { store, flatSchema, contentRoot, config, entryLinkUrl, seo, generatedAt, buildId } = options;
6258
6287
  assertValidNoindexConfig(config);
6259
6288
  assertValidReferenceLink(config);
6260
6289
  const files = /* @__PURE__ */ new Map();
@@ -6265,6 +6294,7 @@ async function generateAIContent(options) {
6265
6294
  config,
6266
6295
  idIndex,
6267
6296
  entryLinkUrl,
6297
+ seo,
6268
6298
  resolveTarget: createReferenceTargetResolver(store)
6269
6299
  };
6270
6300
  const collections = flatSchema.filter((item) => item.type === "collection");
@@ -6416,7 +6446,7 @@ async function collectCollection(read, collection, flatSchema) {
6416
6446
  return { collection, cleanPath, entries, subcollections };
6417
6447
  }
6418
6448
  async function collectEntries(read, collection, cleanPath) {
6419
- const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget: resolveTarget2 } = read;
6449
+ const { store, contentRoot, config, idIndex, entryLinkUrl, seo, resolveTarget: resolveTarget2 } = read;
6420
6450
  const skipNoindex = config?.exclude?.noindex !== false;
6421
6451
  const pending = [];
6422
6452
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
@@ -6434,7 +6464,7 @@ async function collectEntries(read, collection, cleanPath) {
6434
6464
  const doc = await store.read(listEntry.collection, listEntry.slug, {
6435
6465
  resolveReferences: false
6436
6466
  });
6437
- if (skipNoindex && isNoindexEntry(doc.data, config?.seo))
6467
+ if (skipNoindex && isNoindexEntry(doc.data, seo))
6438
6468
  continue;
6439
6469
  const storedData = doc.data;
6440
6470
  doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget2);
@@ -6501,24 +6531,8 @@ function assertValidNoindexConfig(config) {
6501
6531
  if (noindex !== void 0 && typeof noindex !== "boolean") {
6502
6532
  throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
6503
6533
  }
6504
- const seo = config?.seo;
6505
- if (seo === void 0)
6506
- return;
6507
- if (typeof seo !== "object" || seo === null || Array.isArray(seo)) {
6508
- throw new Error("AI content config: seo must be an object of { group?, fields? }");
6509
- }
6510
- const { group, fields } = seo;
6511
- if (group !== void 0 && typeof group !== "string") {
6512
- throw new Error("AI content config: seo.group must be a string");
6513
- }
6514
- if (fields === void 0)
6515
- return;
6516
- if (typeof fields !== "object" || fields === null || Array.isArray(fields) || Object.values(fields).some((name) => typeof name !== "string")) {
6517
- throw new Error("AI content config: seo.fields must map SEO fields to field-name strings");
6518
- }
6519
- const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
6520
- if (unknown.length > 0) {
6521
- throw new Error(`AI content config: unknown seo.fields key(s): ${unknown.join(", ")}`);
6534
+ if (config && "seo" in config) {
6535
+ throw new Error("AI content config: seo is not an AI content option. Set it once as seo in defineCanopyConfig, where the sitemap, page metadata and AI content all read it.");
6522
6536
  }
6523
6537
  }
6524
6538
  function stripContentRoot(logicalPath, contentRoot) {
@@ -7492,6 +7506,14 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
7492
7506
  }
7493
7507
  if (value === void 0 || value === null)
7494
7508
  continue;
7509
+ if (Array.isArray(value) && "maxItems" in field && typeof field.maxItems === "number" && value.length > field.maxItems) {
7510
+ const cap = field.maxItems;
7511
+ errors.push({
7512
+ fieldPath: path272,
7513
+ message: `At most ${cap} ${cap === 1 ? "item" : "items"}; this list has ${value.length}`,
7514
+ maxItems: { count: value.length, cap }
7515
+ });
7516
+ }
7495
7517
  if (field.type === "block") {
7496
7518
  const blockField = field;
7497
7519
  if (!Array.isArray(value)) {
@@ -7590,14 +7612,34 @@ function assertBuildEntriesValid(items, phaseLabel) {
7590
7612
  const invalid = findInvalidEntries(items);
7591
7613
  if (invalid.length === 0)
7592
7614
  return;
7593
- const lines = invalid.map(({ entryPath, errors }) => {
7594
- const [first, ...rest] = errors;
7595
- const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
7596
- return ` - ${entryPath} \u2014 ${summary}`;
7597
- });
7615
+ const lines = [];
7616
+ let hasOverCap = false;
7617
+ let hasOrdinary = false;
7618
+ for (const { entryPath, errors } of invalid) {
7619
+ const ordinary = errors.filter((e) => e.maxItems === void 0);
7620
+ if (ordinary.length > 0) {
7621
+ hasOrdinary = true;
7622
+ const [first, ...rest] = ordinary;
7623
+ const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
7624
+ lines.push(` - ${entryPath} \u2014 ${summary}`);
7625
+ }
7626
+ for (const { fieldPath, maxItems } of errors) {
7627
+ if (maxItems === void 0)
7628
+ continue;
7629
+ hasOverCap = true;
7630
+ lines.push(` - ${entryPath} \u2014 ${fieldPath}: ${maxItems.count} items, but maxItems is ${maxItems.cap}`);
7631
+ }
7632
+ }
7633
+ const advice = [];
7634
+ if (hasOverCap) {
7635
+ advice.push(`A list over its maxItems: open the entry in the editor and remove a pick from that list (or remove items) until it holds at most maxItems, then rebuild. This usually happens when two branches each add one item to the same list and are merged one after the other.`);
7636
+ }
7637
+ if (hasOrdinary) {
7638
+ advice.push(`${hasOverCap ? "The other errors are" : "These are"} likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
7639
+ }
7598
7640
  throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
7599
7641
  ${lines.join("\n")}
7600
- These are likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
7642
+ ` + advice.join("\n"));
7601
7643
  }
7602
7644
  async function readGeneratedRecord(outputDir) {
7603
7645
  try {
@@ -7689,6 +7731,7 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
7689
7731
  contentRoot: contentRootName,
7690
7732
  config: aiConfig2,
7691
7733
  entryLinkUrl: config.entryLinkUrl,
7734
+ seo: config.seo,
7692
7735
  ...resolveBuildStamp()
7693
7736
  });
7694
7737
  const absoluteOutputDir = path25.resolve(outputDir) + path25.sep;
@@ -7785,7 +7828,7 @@ async function generateAIContentCLI(options) {
7785
7828
  console.log(` Output: ${result.outputDir}
7786
7829
  `);
7787
7830
  }
7788
- var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env, forEachReferenceField, init_validation, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned4, SchemaUnavailableError, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, RESERVED_SETTINGS_BRANCH_PREFIX, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, log10, normalizePath, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
7831
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceSortFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, seoFieldNameSchema2, seoSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env, forEachReferenceField, init_validation, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned4, SchemaUnavailableError, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, RESERVED_SETTINGS_BRANCH_PREFIX, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, log10, normalizePath, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
7789
7832
  var init_generate_ai_content = __esm({
7790
7833
  "dist/cli/generate-ai-content.js"() {
7791
7834
  "use strict";
@@ -8068,7 +8111,10 @@ var init_generate_ai_content = __esm({
8068
8111
  required: z22.boolean().optional(),
8069
8112
  list: z22.boolean().optional(),
8070
8113
  isTitle: z22.boolean().optional(),
8071
- isBody: z22.boolean().optional()
8114
+ isBody: z22.boolean().optional(),
8115
+ // Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
8116
+ // `ensureMaxItemsPlacement`'s call (config/validation.ts).
8117
+ maxItems: z22.number().int().positive().optional()
8072
8118
  });
8073
8119
  selectOptionSchema2 = z22.union([
8074
8120
  z22.string(),
@@ -8126,6 +8172,7 @@ var init_generate_ai_content = __esm({
8126
8172
  type: z22.literal("select"),
8127
8173
  options: z22.array(selectOptionSchema2).min(1)
8128
8174
  });
8175
+ referenceSortFieldSchema2 = z22.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
8129
8176
  referenceFieldSchema2 = fieldBaseSchema2.extend({
8130
8177
  type: z22.literal("reference"),
8131
8178
  collections: z22.array(z22.string().min(1)).min(1).optional(),
@@ -8136,7 +8183,14 @@ var init_generate_ai_content = __esm({
8136
8183
  // default, so a consumer that adopts this schema's parse output would silently delete a
8137
8184
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
8138
8185
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
8139
- includeBody: z22.boolean().optional()
8186
+ includeBody: z22.boolean().optional(),
8187
+ optionSort: z22.union([
8188
+ z22.enum(["label", "collection"]),
8189
+ z22.object({
8190
+ field: referenceSortFieldSchema2,
8191
+ direction: z22.enum(["asc", "desc"]).optional()
8192
+ }).strict()
8193
+ ]).optional()
8140
8194
  });
8141
8195
  fieldHolder2 = [z22.never()];
8142
8196
  blockSchema2 = z22.object({
@@ -8314,6 +8368,19 @@ var init_generate_ai_content = __esm({
8314
8368
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
8315
8369
  SignInComponent: z6.custom().optional()
8316
8370
  });
8371
+ seoFieldNameSchema2 = z6.string().min(1).optional();
8372
+ seoSchema2 = z6.object({
8373
+ group: z6.string().min(1).optional(),
8374
+ fields: z6.object({
8375
+ title: seoFieldNameSchema2,
8376
+ description: seoFieldNameSchema2,
8377
+ ogImage: seoFieldNameSchema2,
8378
+ ogType: seoFieldNameSchema2,
8379
+ canonical: seoFieldNameSchema2,
8380
+ noindex: seoFieldNameSchema2,
8381
+ twitterCard: seoFieldNameSchema2
8382
+ }).strict().optional()
8383
+ }).strict();
8317
8384
  CanopyConfigSchema2 = z6.object({
8318
8385
  media: mediaSchema2.optional(),
8319
8386
  // No outer .optional(): both schemas already .default('deny'), so the field is optional on
@@ -8363,6 +8430,7 @@ var init_generate_ai_content = __esm({
8363
8430
  entryLinkUrl: z6.custom().optional(),
8364
8431
  validateEntry: z6.custom().optional(),
8365
8432
  mdxAllow: mdxAllowlistSchema2.optional(),
8433
+ seo: seoSchema2.optional(),
8366
8434
  dev: devConfigSchema2.optional()
8367
8435
  }).strict();
8368
8436
  DEFAULT_PROD_WORKSPACE2 = "/mnt/efs";
@@ -15322,7 +15390,7 @@ var init_mdx_allowlist3 = __esm({
15322
15390
 
15323
15391
  // dist/config/schemas/field.js
15324
15392
  import { z as z10 } from "zod";
15325
- var fieldBaseSchema3, selectOptionSchema3, referenceOptionSchema3, primitiveFieldSchema3, mdxPropAllowSchema3, mdxAllowlistSchema3, markdownFieldSchema3, ASPECT_RATIO_RE3, imageFieldSchema3, selectFieldSchema3, referenceFieldSchema3, fieldHolder3, blockSchema3, blockFieldSchema3, objectFieldSchema3, inlineGroupFieldSchema3, customFieldSchema3, knownFieldSchema3, fieldSchema3;
15393
+ var fieldBaseSchema3, selectOptionSchema3, referenceOptionSchema3, primitiveFieldSchema3, mdxPropAllowSchema3, mdxAllowlistSchema3, markdownFieldSchema3, ASPECT_RATIO_RE3, imageFieldSchema3, selectFieldSchema3, referenceSortFieldSchema3, referenceFieldSchema3, fieldHolder3, blockSchema3, blockFieldSchema3, objectFieldSchema3, inlineGroupFieldSchema3, customFieldSchema3, knownFieldSchema3, fieldSchema3;
15326
15394
  var init_field3 = __esm({
15327
15395
  "dist/config/schemas/field.js"() {
15328
15396
  "use strict";
@@ -15335,7 +15403,10 @@ var init_field3 = __esm({
15335
15403
  required: z10.boolean().optional(),
15336
15404
  list: z10.boolean().optional(),
15337
15405
  isTitle: z10.boolean().optional(),
15338
- isBody: z10.boolean().optional()
15406
+ isBody: z10.boolean().optional(),
15407
+ // Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
15408
+ // `ensureMaxItemsPlacement`'s call (config/validation.ts).
15409
+ maxItems: z10.number().int().positive().optional()
15339
15410
  });
15340
15411
  selectOptionSchema3 = z10.union([
15341
15412
  z10.string(),
@@ -15393,6 +15464,7 @@ var init_field3 = __esm({
15393
15464
  type: z10.literal("select"),
15394
15465
  options: z10.array(selectOptionSchema3).min(1)
15395
15466
  });
15467
+ referenceSortFieldSchema3 = z10.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
15396
15468
  referenceFieldSchema3 = fieldBaseSchema3.extend({
15397
15469
  type: z10.literal("reference"),
15398
15470
  collections: z10.array(z10.string().min(1)).min(1).optional(),
@@ -15403,7 +15475,14 @@ var init_field3 = __esm({
15403
15475
  // default, so a consumer that adopts this schema's parse output would silently delete a
15404
15476
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
15405
15477
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
15406
- includeBody: z10.boolean().optional()
15478
+ includeBody: z10.boolean().optional(),
15479
+ optionSort: z10.union([
15480
+ z10.enum(["label", "collection"]),
15481
+ z10.object({
15482
+ field: referenceSortFieldSchema3,
15483
+ direction: z10.enum(["asc", "desc"]).optional()
15484
+ }).strict()
15485
+ ]).optional()
15407
15486
  });
15408
15487
  fieldHolder3 = [z10.never()];
15409
15488
  blockSchema3 = z10.object({
@@ -15637,7 +15716,7 @@ var init_deployment_name2 = __esm({
15637
15716
 
15638
15717
  // dist/config/schemas/config.js
15639
15718
  import { z as z14 } from "zod";
15640
- var defaultBranchAccessSchema3, defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3, defaultPathAccessSchema3, defaultBaseBranchSchema3, defaultRemoteNameSchema3, defaultRemoteUrlSchema3, gitBotAuthorNameSchema3, gitBotAuthorEmailSchema3, githubTokenEnvVarSchema3, operatingModeSchema3, deployedAsSchema3, contentRootSchema3, sourceRootSchema3, basePathSchema3, deploymentNameSchema3, devContentSyncSchema3, devConfigSchema3, editorConfigSchema3, CanopyConfigSchema3, DEFAULT_PROD_WORKSPACE3;
15719
+ var defaultBranchAccessSchema3, defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3, defaultPathAccessSchema3, defaultBaseBranchSchema3, defaultRemoteNameSchema3, defaultRemoteUrlSchema3, gitBotAuthorNameSchema3, gitBotAuthorEmailSchema3, githubTokenEnvVarSchema3, operatingModeSchema3, deployedAsSchema3, contentRootSchema3, sourceRootSchema3, basePathSchema3, deploymentNameSchema3, devContentSyncSchema3, devConfigSchema3, editorConfigSchema3, seoFieldNameSchema3, seoSchema3, CanopyConfigSchema3, DEFAULT_PROD_WORKSPACE3;
15641
15720
  var init_config4 = __esm({
15642
15721
  "dist/config/schemas/config.js"() {
15643
15722
  "use strict";
@@ -15684,6 +15763,19 @@ var init_config4 = __esm({
15684
15763
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
15685
15764
  SignInComponent: z14.custom().optional()
15686
15765
  });
15766
+ seoFieldNameSchema3 = z14.string().min(1).optional();
15767
+ seoSchema3 = z14.object({
15768
+ group: z14.string().min(1).optional(),
15769
+ fields: z14.object({
15770
+ title: seoFieldNameSchema3,
15771
+ description: seoFieldNameSchema3,
15772
+ ogImage: seoFieldNameSchema3,
15773
+ ogType: seoFieldNameSchema3,
15774
+ canonical: seoFieldNameSchema3,
15775
+ noindex: seoFieldNameSchema3,
15776
+ twitterCard: seoFieldNameSchema3
15777
+ }).strict().optional()
15778
+ }).strict();
15687
15779
  CanopyConfigSchema3 = z14.object({
15688
15780
  media: mediaSchema3.optional(),
15689
15781
  // No outer .optional(): both schemas already .default('deny'), so the field is optional on
@@ -15733,6 +15825,7 @@ var init_config4 = __esm({
15733
15825
  entryLinkUrl: z14.custom().optional(),
15734
15826
  validateEntry: z14.custom().optional(),
15735
15827
  mdxAllow: mdxAllowlistSchema3.optional(),
15828
+ seo: seoSchema3.optional(),
15736
15829
  dev: devConfigSchema3.optional()
15737
15830
  }).strict();
15738
15831
  DEFAULT_PROD_WORKSPACE3 = "/mnt/efs";
@@ -599,7 +599,7 @@ var init_mdx_allowlist = __esm({
599
599
 
600
600
  // dist/config/schemas/field.js
601
601
  import { z as z2 } from "zod";
602
- var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
602
+ var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
603
603
  var init_field = __esm({
604
604
  "dist/config/schemas/field.js"() {
605
605
  "use strict";
@@ -612,7 +612,10 @@ var init_field = __esm({
612
612
  required: z2.boolean().optional(),
613
613
  list: z2.boolean().optional(),
614
614
  isTitle: z2.boolean().optional(),
615
- isBody: z2.boolean().optional()
615
+ isBody: z2.boolean().optional(),
616
+ // Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
617
+ // `ensureMaxItemsPlacement`'s call (config/validation.ts).
618
+ maxItems: z2.number().int().positive().optional()
616
619
  });
617
620
  selectOptionSchema = z2.union([
618
621
  z2.string(),
@@ -670,6 +673,7 @@ var init_field = __esm({
670
673
  type: z2.literal("select"),
671
674
  options: z2.array(selectOptionSchema).min(1)
672
675
  });
676
+ referenceSortFieldSchema = z2.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
673
677
  referenceFieldSchema = fieldBaseSchema.extend({
674
678
  type: z2.literal("reference"),
675
679
  collections: z2.array(z2.string().min(1)).min(1).optional(),
@@ -680,7 +684,14 @@ var init_field = __esm({
680
684
  // default, so a consumer that adopts this schema's parse output would silently delete a
681
685
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
682
686
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
683
- includeBody: z2.boolean().optional()
687
+ includeBody: z2.boolean().optional(),
688
+ optionSort: z2.union([
689
+ z2.enum(["label", "collection"]),
690
+ z2.object({
691
+ field: referenceSortFieldSchema,
692
+ direction: z2.enum(["asc", "desc"]).optional()
693
+ }).strict()
694
+ ]).optional()
684
695
  });
685
696
  fieldHolder = [z2.never()];
686
697
  blockSchema = z2.object({
@@ -828,7 +839,7 @@ var init_media = __esm({
828
839
 
829
840
  // dist/config/schemas/config.js
830
841
  import { z as z6 } from "zod";
831
- var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
842
+ var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
832
843
  var init_config = __esm({
833
844
  "dist/config/schemas/config.js"() {
834
845
  "use strict";
@@ -875,6 +886,19 @@ var init_config = __esm({
875
886
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
876
887
  SignInComponent: z6.custom().optional()
877
888
  });
889
+ seoFieldNameSchema = z6.string().min(1).optional();
890
+ seoSchema = z6.object({
891
+ group: z6.string().min(1).optional(),
892
+ fields: z6.object({
893
+ title: seoFieldNameSchema,
894
+ description: seoFieldNameSchema,
895
+ ogImage: seoFieldNameSchema,
896
+ ogType: seoFieldNameSchema,
897
+ canonical: seoFieldNameSchema,
898
+ noindex: seoFieldNameSchema,
899
+ twitterCard: seoFieldNameSchema
900
+ }).strict().optional()
901
+ }).strict();
878
902
  CanopyConfigSchema = z6.object({
879
903
  media: mediaSchema.optional(),
880
904
  // No outer .optional(): both schemas already .default('deny'), so the field is optional on
@@ -924,6 +948,7 @@ var init_config = __esm({
924
948
  entryLinkUrl: z6.custom().optional(),
925
949
  validateEntry: z6.custom().optional(),
926
950
  mdxAllow: mdxAllowlistSchema.optional(),
951
+ seo: seoSchema.optional(),
927
952
  dev: devConfigSchema.optional()
928
953
  }).strict();
929
954
  DEFAULT_PROD_WORKSPACE = "/mnt/efs";
@@ -2980,7 +3005,11 @@ var asTwitterCard = (value) => {
2980
3005
  return text === "summary" || text === "summary_large_image" ? text : void 0;
2981
3006
  };
2982
3007
  function extractSeoFields(entryData, opts = {}) {
2983
- const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
3008
+ const names = { ...DEFAULT_SEO_FIELD_NAMES };
3009
+ for (const [key, name] of Object.entries(opts.fields ?? {})) {
3010
+ if (typeof name === "string" && name !== "")
3011
+ names[key] = name;
3012
+ }
2984
3013
  const entry = asRecord2(entryData);
2985
3014
  const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
2986
3015
  return {
@@ -6723,7 +6752,7 @@ function createReferenceRendering(idIndex, flatSchema, entryLinkUrl, markdownCop
6723
6752
 
6724
6753
  // dist/ai/generate.js
6725
6754
  async function generateAIContent(options) {
6726
- const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
6755
+ const { store, flatSchema, contentRoot, config, entryLinkUrl, seo, generatedAt, buildId } = options;
6727
6756
  assertValidNoindexConfig(config);
6728
6757
  assertValidReferenceLink(config);
6729
6758
  const files = /* @__PURE__ */ new Map();
@@ -6734,6 +6763,7 @@ async function generateAIContent(options) {
6734
6763
  config,
6735
6764
  idIndex,
6736
6765
  entryLinkUrl,
6766
+ seo,
6737
6767
  resolveTarget: createReferenceTargetResolver(store)
6738
6768
  };
6739
6769
  const collections = flatSchema.filter((item) => item.type === "collection");
@@ -6885,7 +6915,7 @@ async function collectCollection(read, collection, flatSchema) {
6885
6915
  return { collection, cleanPath, entries, subcollections };
6886
6916
  }
6887
6917
  async function collectEntries(read, collection, cleanPath) {
6888
- const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget } = read;
6918
+ const { store, contentRoot, config, idIndex, entryLinkUrl, seo, resolveTarget } = read;
6889
6919
  const skipNoindex = config?.exclude?.noindex !== false;
6890
6920
  const pending = [];
6891
6921
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
@@ -6903,7 +6933,7 @@ async function collectEntries(read, collection, cleanPath) {
6903
6933
  const doc = await store.read(listEntry.collection, listEntry.slug, {
6904
6934
  resolveReferences: false
6905
6935
  });
6906
- if (skipNoindex && isNoindexEntry(doc.data, config?.seo))
6936
+ if (skipNoindex && isNoindexEntry(doc.data, seo))
6907
6937
  continue;
6908
6938
  const storedData = doc.data;
6909
6939
  doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget);
@@ -6970,24 +7000,8 @@ function assertValidNoindexConfig(config) {
6970
7000
  if (noindex !== void 0 && typeof noindex !== "boolean") {
6971
7001
  throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
6972
7002
  }
6973
- const seo = config?.seo;
6974
- if (seo === void 0)
6975
- return;
6976
- if (typeof seo !== "object" || seo === null || Array.isArray(seo)) {
6977
- throw new Error("AI content config: seo must be an object of { group?, fields? }");
6978
- }
6979
- const { group, fields } = seo;
6980
- if (group !== void 0 && typeof group !== "string") {
6981
- throw new Error("AI content config: seo.group must be a string");
6982
- }
6983
- if (fields === void 0)
6984
- return;
6985
- if (typeof fields !== "object" || fields === null || Array.isArray(fields) || Object.values(fields).some((name) => typeof name !== "string")) {
6986
- throw new Error("AI content config: seo.fields must map SEO fields to field-name strings");
6987
- }
6988
- const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
6989
- if (unknown.length > 0) {
6990
- throw new Error(`AI content config: unknown seo.fields key(s): ${unknown.join(", ")}`);
7003
+ if (config && "seo" in config) {
7004
+ throw new Error("AI content config: seo is not an AI content option. Set it once as seo in defineCanopyConfig, where the sitemap, page metadata and AI content all read it.");
6991
7005
  }
6992
7006
  }
6993
7007
  function stripContentRoot(logicalPath, contentRoot) {
@@ -10138,6 +10152,14 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
10138
10152
  }
10139
10153
  if (value === void 0 || value === null)
10140
10154
  continue;
10155
+ if (Array.isArray(value) && "maxItems" in field && typeof field.maxItems === "number" && value.length > field.maxItems) {
10156
+ const cap = field.maxItems;
10157
+ errors.push({
10158
+ fieldPath: path27,
10159
+ message: `At most ${cap} ${cap === 1 ? "item" : "items"}; this list has ${value.length}`,
10160
+ maxItems: { count: value.length, cap }
10161
+ });
10162
+ }
10141
10163
  if (field.type === "block") {
10142
10164
  const blockField = field;
10143
10165
  if (!Array.isArray(value)) {
@@ -10238,14 +10260,34 @@ function assertBuildEntriesValid(items, phaseLabel) {
10238
10260
  const invalid = findInvalidEntries(items);
10239
10261
  if (invalid.length === 0)
10240
10262
  return;
10241
- const lines = invalid.map(({ entryPath, errors }) => {
10242
- const [first, ...rest] = errors;
10243
- const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
10244
- return ` - ${entryPath} \u2014 ${summary}`;
10245
- });
10263
+ const lines = [];
10264
+ let hasOverCap = false;
10265
+ let hasOrdinary = false;
10266
+ for (const { entryPath, errors } of invalid) {
10267
+ const ordinary = errors.filter((e) => e.maxItems === void 0);
10268
+ if (ordinary.length > 0) {
10269
+ hasOrdinary = true;
10270
+ const [first, ...rest] = ordinary;
10271
+ const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
10272
+ lines.push(` - ${entryPath} \u2014 ${summary}`);
10273
+ }
10274
+ for (const { fieldPath, maxItems } of errors) {
10275
+ if (maxItems === void 0)
10276
+ continue;
10277
+ hasOverCap = true;
10278
+ lines.push(` - ${entryPath} \u2014 ${fieldPath}: ${maxItems.count} items, but maxItems is ${maxItems.cap}`);
10279
+ }
10280
+ }
10281
+ const advice = [];
10282
+ if (hasOverCap) {
10283
+ advice.push(`A list over its maxItems: open the entry in the editor and remove a pick from that list (or remove items) until it holds at most maxItems, then rebuild. This usually happens when two branches each add one item to the same list and are merged one after the other.`);
10284
+ }
10285
+ if (hasOrdinary) {
10286
+ advice.push(`${hasOverCap ? "The other errors are" : "These are"} likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
10287
+ }
10246
10288
  throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
10247
10289
  ${lines.join("\n")}
10248
- These are likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
10290
+ ` + advice.join("\n"));
10249
10291
  }
10250
10292
 
10251
10293
  // dist/build/generate-ai-content.js
@@ -10342,6 +10384,7 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
10342
10384
  contentRoot: contentRootName,
10343
10385
  config: aiConfig,
10344
10386
  entryLinkUrl: config.entryLinkUrl,
10387
+ seo: config.seo,
10345
10388
  ...resolveBuildStamp()
10346
10389
  });
10347
10390
  const absoluteOutputDir = path25.resolve(outputDir) + path25.sep;