canopycms 0.3.3 → 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 (69) hide show
  1. package/CHANGELOG.md +91 -41
  2. package/dist/ai/generate.d.ts +3 -0
  3. package/dist/ai/generate.js +23 -33
  4. package/dist/ai/handler.js +1 -0
  5. package/dist/ai/types.d.ts +6 -11
  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 +176 -66
  11. package/dist/cli/generate-ai-content.js +122 -62
  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 +47 -8
  19. package/dist/config/validation.d.ts +6 -0
  20. package/dist/config/validation.js +33 -0
  21. package/dist/content-store.d.ts +5 -4
  22. package/dist/content-store.js +12 -7
  23. package/dist/editor/FormRenderer.js +15 -13
  24. package/dist/editor/copy.d.ts +5 -0
  25. package/dist/editor/copy.js +5 -0
  26. package/dist/editor/fields/BlockField.d.ts +2 -0
  27. package/dist/editor/fields/BlockField.js +15 -8
  28. package/dist/editor/fields/ItemList.d.ts +21 -3
  29. package/dist/editor/fields/ItemList.js +16 -8
  30. package/dist/editor/fields/NumberListField.d.ts +2 -0
  31. package/dist/editor/fields/NumberListField.js +6 -2
  32. package/dist/editor/fields/ObjectListField.js +9 -3
  33. package/dist/editor/fields/ReferenceField.d.ts +6 -7
  34. package/dist/editor/fields/ReferenceField.js +28 -122
  35. package/dist/editor/fields/ReferenceListField.d.ts +41 -0
  36. package/dist/editor/fields/ReferenceListField.js +139 -0
  37. package/dist/editor/fields/SelectField.d.ts +2 -0
  38. package/dist/editor/fields/SelectField.js +6 -3
  39. package/dist/editor/fields/StringListField.d.ts +10 -1
  40. package/dist/editor/fields/StringListField.js +18 -8
  41. package/dist/editor/fields/item-expansion.d.ts +7 -3
  42. package/dist/editor/fields/item-expansion.js +6 -4
  43. package/dist/editor/fields/list-item-keys.d.ts +6 -5
  44. package/dist/editor/fields/list-item-keys.js +45 -13
  45. package/dist/editor/fields/reference-noindex.d.ts +37 -0
  46. package/dist/editor/fields/reference-noindex.js +48 -0
  47. package/dist/editor/fields/use-reference-options.d.ts +36 -0
  48. package/dist/editor/fields/use-reference-options.js +121 -0
  49. package/dist/editor/form-outline.d.ts +2 -2
  50. package/dist/editor/form-outline.js +4 -2
  51. package/dist/editor/theme.js +5 -0
  52. package/dist/entry-schema-registry.d.ts +1 -1
  53. package/dist/entry-schema-registry.js +3 -2
  54. package/dist/entry-schema.d.ts +21 -9
  55. package/dist/entry-schema.js +5 -2
  56. package/dist/reference-resolver.d.ts +18 -1
  57. package/dist/reference-resolver.js +175 -8
  58. package/dist/schema/schema-store.d.ts +2 -2
  59. package/dist/static/index.d.ts +8 -4
  60. package/dist/static/index.js +38 -13
  61. package/dist/static/seo.d.ts +7 -5
  62. package/dist/static/seo.js +13 -6
  63. package/dist/utils/body-field.d.ts +9 -1
  64. package/dist/utils/body-field.js +18 -1
  65. package/dist/validation/entry-validator.d.ts +5 -0
  66. package/dist/validation/entry-validator.js +12 -0
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { ContentStore } from '../content-store.js';
3
3
  import { defineEndpoint } from './route-builder.js';
4
4
  import { ReferenceResolver } from '../reference-resolver.js';
5
+ import { referenceSortFieldSchema } from '../config/schemas/field.js';
5
6
  import { parseLogicalPath } from '../paths/index.js';
6
7
  import { branchNameSchema } from './validators.js';
7
8
  const getReferenceOptionsParamsSchema = z.object({
@@ -16,9 +17,18 @@ const getReferenceOptionsHandler = async (gc, ctx, req, _params) => {
16
17
  entryTypes: z.string().optional(),
17
18
  displayField: z.string().optional(),
18
19
  search: z.string().optional(),
20
+ sort: z.enum(['label', 'collection', 'field']).optional(),
21
+ sortField: referenceSortFieldSchema.optional(),
22
+ sortDir: z.enum(['asc', 'desc']).optional(),
19
23
  })
20
24
  .refine((data) => data.collections || data.entryTypes, {
21
25
  message: 'At least one of "collections" or "entryTypes" query parameters is required',
26
+ })
27
+ .refine((data) => (data.sort === 'field') === (data.sortField !== undefined), {
28
+ message: '"sortField" is required with sort=field, and only allowed with it',
29
+ })
30
+ .refine((data) => data.sortDir === undefined || data.sort === 'field', {
31
+ message: '"sortDir" is only allowed with sort=field',
22
32
  });
23
33
  const queryResult = querySchema.safeParse(req.query ?? {});
24
34
  if (!queryResult.success) {
@@ -32,6 +42,13 @@ const getReferenceOptionsHandler = async (gc, ctx, req, _params) => {
32
42
  const entryTypesParam = queryResult.data.entryTypes;
33
43
  const displayField = queryResult.data.displayField || 'title';
34
44
  const search = queryResult.data.search;
45
+ // The refinements above pair sortField with sort=field, so sortField alone decides.
46
+ const { sort: sortParam, sortField, sortDir } = queryResult.data;
47
+ const sort = sortField !== undefined
48
+ ? { field: sortField, direction: sortDir }
49
+ : sortParam === 'field'
50
+ ? undefined
51
+ : sortParam;
35
52
  const flatSchema = branchContext.flatSchema;
36
53
  const store = new ContentStore(branchContext.branchRoot, flatSchema, {
37
54
  contentRootName: ctx.services.config.contentRoot || 'content',
@@ -70,7 +87,7 @@ const getReferenceOptionsHandler = async (gc, ctx, req, _params) => {
70
87
  // for an option the caller isn't allowed to see.
71
88
  const checkAccess = await ctx.services.createContentAccessChecker(branchContext, branchContext.branchRoot, req.user);
72
89
  const resolver = new ReferenceResolver(store, idIndex);
73
- const options = await resolver.loadReferenceOptions(collections, displayField, search, entryTypes, (logicalPath) => checkAccess(logicalPath, 'read').allowed);
90
+ const options = await resolver.loadReferenceOptions(collections, displayField, search, entryTypes, (logicalPath) => checkAccess(logicalPath, 'read').allowed, { seo: ctx.services.config.seo, sort });
74
91
  return {
75
92
  ok: true,
76
93
  status: 200,
@@ -81,7 +98,9 @@ const getReferenceOptionsHandler = async (gc, ctx, req, _params) => {
81
98
  * Get reference options for a field
82
99
  * GET /:branch/reference-options
83
100
  * Query params: collections (comma-separated, optional), entryTypes (comma-separated, optional),
84
- * displayField, search. At least one of collections or entryTypes is required.
101
+ * displayField, search. At least one of collections or entryTypes is required. `sort` is
102
+ * label (default), collection or field; field takes `sortField` (a dotted path) and `sortDir`
103
+ * (asc, the default, or desc). See `ReferenceOptionSort`.
85
104
  */
86
105
  const getReferenceOptions = defineEndpoint({
87
106
  namespace: 'content',
@@ -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";
@@ -4552,16 +4577,30 @@ function flattenGroupFields(fields) {
4552
4577
  return result;
4553
4578
  }
4554
4579
  function findBodyFieldName(fields) {
4580
+ return findExplicitBodyFieldName(fields) ?? "body";
4581
+ }
4582
+ function findExplicitBodyFieldName(fields) {
4555
4583
  for (const field of flattenGroupFields(fields)) {
4556
4584
  if ("isBody" in field && field.isBody)
4557
4585
  return field.name;
4558
4586
  }
4559
- return "body";
4587
+ return void 0;
4588
+ }
4589
+ function linkOnlyReferenceData(data, format, fields) {
4590
+ const bodyFieldName = isDataOnlyFormat(format) && fields ? findExplicitBodyFieldName(fields) : void 0;
4591
+ if (bodyFieldName === void 0 || !(bodyFieldName in data))
4592
+ return data;
4593
+ const { [bodyFieldName]: _body, ...rest } = data;
4594
+ return rest;
4560
4595
  }
4561
4596
  function extractSeoFields(entryData, opts = {}) {
4562
- const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
4563
- const entry = asRecord(entryData);
4564
- const source = opts.group ? asRecord(entry?.[opts.group]) : entry;
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
+ }
4602
+ const entry = asRecord2(entryData);
4603
+ const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
4565
4604
  return {
4566
4605
  title: asText(source?.[names.title]) ?? asText(opts.fallbackTitle),
4567
4606
  description: asText(source?.[names.description]) ?? asText(opts.fallbackDescription),
@@ -6244,7 +6283,7 @@ function createReferenceRendering(idIndex, flatSchema, entryLinkUrl, markdownCop
6244
6283
  };
6245
6284
  }
6246
6285
  async function generateAIContent(options) {
6247
- const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
6286
+ const { store, flatSchema, contentRoot, config, entryLinkUrl, seo, generatedAt, buildId } = options;
6248
6287
  assertValidNoindexConfig(config);
6249
6288
  assertValidReferenceLink(config);
6250
6289
  const files = /* @__PURE__ */ new Map();
@@ -6255,6 +6294,7 @@ async function generateAIContent(options) {
6255
6294
  config,
6256
6295
  idIndex,
6257
6296
  entryLinkUrl,
6297
+ seo,
6258
6298
  resolveTarget: createReferenceTargetResolver(store)
6259
6299
  };
6260
6300
  const collections = flatSchema.filter((item) => item.type === "collection");
@@ -6285,14 +6325,18 @@ async function generateAIContent(options) {
6285
6325
  const pending = pendingById.get(id);
6286
6326
  if (!pending || !exported.has(id))
6287
6327
  return void 0;
6288
- const { slug } = pending.entry;
6289
- const target = buildResolvedReference(structuredClone(pending.storedData), {
6328
+ const { format } = pending;
6329
+ const { slug, fields } = pending.entry;
6330
+ const storedData = structuredClone(pending.storedData);
6331
+ const target = buildResolvedReference(linkOnlyReferenceData(storedData, format, fields), {
6290
6332
  id,
6291
6333
  slug,
6292
6334
  collection: pending.collectionPath,
6293
6335
  urlPath: computeEntryUrl(pending.collectionPath, slug, contentRoot)
6294
6336
  });
6295
- return { target, body: pending.body };
6337
+ const dataBodyField = isDataOnlyFormat(format) ? findExplicitBodyFieldName(fields) : void 0;
6338
+ const dataBody = dataBodyField === void 0 ? void 0 : storedData[dataBodyField];
6339
+ return { target, body: pending.body ?? (typeof dataBody === "string" ? dataBody : void 0) };
6296
6340
  };
6297
6341
  const linkFor = config?.referenceLink ? await decideReferenceLinks(config.referenceLink, allPending, subjectOf, entryLinkUrl) : void 0;
6298
6342
  const bodyLinkUrl = linkFor ? (location) => {
@@ -6402,7 +6446,7 @@ async function collectCollection(read, collection, flatSchema) {
6402
6446
  return { collection, cleanPath, entries, subcollections };
6403
6447
  }
6404
6448
  async function collectEntries(read, collection, cleanPath) {
6405
- const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget: resolveTarget2 } = read;
6449
+ const { store, contentRoot, config, idIndex, entryLinkUrl, seo, resolveTarget: resolveTarget2 } = read;
6406
6450
  const skipNoindex = config?.exclude?.noindex !== false;
6407
6451
  const pending = [];
6408
6452
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
@@ -6420,7 +6464,7 @@ async function collectEntries(read, collection, cleanPath) {
6420
6464
  const doc = await store.read(listEntry.collection, listEntry.slug, {
6421
6465
  resolveReferences: false
6422
6466
  });
6423
- if (skipNoindex && isNoindexEntry(doc.data, config?.seo))
6467
+ if (skipNoindex && isNoindexEntry(doc.data, seo))
6424
6468
  continue;
6425
6469
  const storedData = doc.data;
6426
6470
  doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget2);
@@ -6434,6 +6478,7 @@ async function collectEntries(read, collection, cleanPath) {
6434
6478
  entry: aiEntry,
6435
6479
  body,
6436
6480
  storedData,
6481
+ format: doc.format,
6437
6482
  linkedIds: [],
6438
6483
  contentId: extractIdFromFilename(path13.basename(listEntry.relativePath)),
6439
6484
  absolutePath: doc.absolutePath,
@@ -6486,24 +6531,8 @@ function assertValidNoindexConfig(config) {
6486
6531
  if (noindex !== void 0 && typeof noindex !== "boolean") {
6487
6532
  throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
6488
6533
  }
6489
- const seo = config?.seo;
6490
- if (seo === void 0)
6491
- return;
6492
- if (typeof seo !== "object" || seo === null || Array.isArray(seo)) {
6493
- throw new Error("AI content config: seo must be an object of { group?, fields? }");
6494
- }
6495
- const { group, fields } = seo;
6496
- if (group !== void 0 && typeof group !== "string") {
6497
- throw new Error("AI content config: seo.group must be a string");
6498
- }
6499
- if (fields === void 0)
6500
- return;
6501
- if (typeof fields !== "object" || fields === null || Array.isArray(fields) || Object.values(fields).some((name) => typeof name !== "string")) {
6502
- throw new Error("AI content config: seo.fields must map SEO fields to field-name strings");
6503
- }
6504
- const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
6505
- if (unknown.length > 0) {
6506
- 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.");
6507
6536
  }
6508
6537
  }
6509
6538
  function stripContentRoot(logicalPath, contentRoot) {
@@ -7477,6 +7506,14 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
7477
7506
  }
7478
7507
  if (value === void 0 || value === null)
7479
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
+ }
7480
7517
  if (field.type === "block") {
7481
7518
  const blockField = field;
7482
7519
  if (!Array.isArray(value)) {
@@ -7575,14 +7612,34 @@ function assertBuildEntriesValid(items, phaseLabel) {
7575
7612
  const invalid = findInvalidEntries(items);
7576
7613
  if (invalid.length === 0)
7577
7614
  return;
7578
- const lines = invalid.map(({ entryPath, errors }) => {
7579
- const [first, ...rest] = errors;
7580
- const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
7581
- return ` - ${entryPath} \u2014 ${summary}`;
7582
- });
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
+ }
7583
7640
  throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
7584
7641
  ${lines.join("\n")}
7585
- 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"));
7586
7643
  }
7587
7644
  async function readGeneratedRecord(outputDir) {
7588
7645
  try {
@@ -7674,6 +7731,7 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
7674
7731
  contentRoot: contentRootName,
7675
7732
  config: aiConfig2,
7676
7733
  entryLinkUrl: config.entryLinkUrl,
7734
+ seo: config.seo,
7677
7735
  ...resolveBuildStamp()
7678
7736
  });
7679
7737
  const absoluteOutputDir = path25.resolve(outputDir) + path25.sep;
@@ -7770,7 +7828,7 @@ async function generateAIContentCLI(options) {
7770
7828
  console.log(` Output: ${result.outputDir}
7771
7829
  `);
7772
7830
  }
7773
- 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, DEFAULT_SEO_FIELD_NAMES, asRecord, 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, getFormatExtension, isDataOnlyFormat, asRecord2, 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;
7774
7832
  var init_generate_ai_content = __esm({
7775
7833
  "dist/cli/generate-ai-content.js"() {
7776
7834
  "use strict";
@@ -8053,7 +8111,10 @@ var init_generate_ai_content = __esm({
8053
8111
  required: z22.boolean().optional(),
8054
8112
  list: z22.boolean().optional(),
8055
8113
  isTitle: z22.boolean().optional(),
8056
- 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()
8057
8118
  });
8058
8119
  selectOptionSchema2 = z22.union([
8059
8120
  z22.string(),
@@ -8111,6 +8172,7 @@ var init_generate_ai_content = __esm({
8111
8172
  type: z22.literal("select"),
8112
8173
  options: z22.array(selectOptionSchema2).min(1)
8113
8174
  });
8175
+ referenceSortFieldSchema2 = z22.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
8114
8176
  referenceFieldSchema2 = fieldBaseSchema2.extend({
8115
8177
  type: z22.literal("reference"),
8116
8178
  collections: z22.array(z22.string().min(1)).min(1).optional(),
@@ -8121,7 +8183,14 @@ var init_generate_ai_content = __esm({
8121
8183
  // default, so a consumer that adopts this schema's parse output would silently delete a
8122
8184
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
8123
8185
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
8124
- 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()
8125
8194
  });
8126
8195
  fieldHolder2 = [z22.never()];
8127
8196
  blockSchema2 = z22.object({
@@ -8299,6 +8368,19 @@ var init_generate_ai_content = __esm({
8299
8368
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
8300
8369
  SignInComponent: z6.custom().optional()
8301
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();
8302
8384
  CanopyConfigSchema2 = z6.object({
8303
8385
  media: mediaSchema2.optional(),
8304
8386
  // No outer .optional(): both schemas already .default('deny'), so the field is optional on
@@ -8348,6 +8430,7 @@ var init_generate_ai_content = __esm({
8348
8430
  entryLinkUrl: z6.custom().optional(),
8349
8431
  validateEntry: z6.custom().optional(),
8350
8432
  mdxAllow: mdxAllowlistSchema2.optional(),
8433
+ seo: seoSchema2.optional(),
8351
8434
  dev: devConfigSchema2.optional()
8352
8435
  }).strict();
8353
8436
  DEFAULT_PROD_WORKSPACE2 = "/mnt/efs";
@@ -9216,6 +9299,17 @@ var init_generate_ai_content = __esm({
9216
9299
  });
9217
9300
  CONTENT_WRITE_LOCK_STALE_MS = 3e4;
9218
9301
  heldRoots = new AsyncLocalStorage();
9302
+ getFormatExtension = (format) => {
9303
+ if (format === "md")
9304
+ return ".md";
9305
+ if (format === "mdx")
9306
+ return ".mdx";
9307
+ if (format === "yaml")
9308
+ return ".yaml";
9309
+ return ".json";
9310
+ };
9311
+ isDataOnlyFormat = (format) => format === "json" || format === "yaml";
9312
+ asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9219
9313
  init_item_keys();
9220
9314
  DEFAULT_SEO_FIELD_NAMES = {
9221
9315
  title: "metaTitle",
@@ -9226,7 +9320,7 @@ var init_generate_ai_content = __esm({
9226
9320
  noindex: "noindex",
9227
9321
  twitterCard: "twitterCard"
9228
9322
  };
9229
- asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
9323
+ asRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
9230
9324
  asText = (value) => {
9231
9325
  if (typeof value !== "string")
9232
9326
  return void 0;
@@ -9238,7 +9332,7 @@ var init_generate_ai_content = __esm({
9238
9332
  const text3 = asText(value);
9239
9333
  if (text3)
9240
9334
  return text3;
9241
- return asText(asRecord(value)?.src);
9335
+ return asText(asRecord2(value)?.src);
9242
9336
  };
9243
9337
  asOgType = (value) => {
9244
9338
  const text3 = asText(value);
@@ -9939,17 +10033,6 @@ var init_generate_ai_content = __esm({
9939
10033
  RESOURCE = "content-index";
9940
10034
  init_error3();
9941
10035
  init_error3();
9942
- getFormatExtension = (format) => {
9943
- if (format === "md")
9944
- return ".md";
9945
- if (format === "mdx")
9946
- return ".mdx";
9947
- if (format === "yaml")
9948
- return ".yaml";
9949
- return ".json";
9950
- };
9951
- isDataOnlyFormat = (format) => format === "json" || format === "yaml";
9952
- asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9953
10036
  init_normalize();
9954
10037
  init_operating_mode();
9955
10038
  RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
@@ -10448,7 +10531,7 @@ var init_generate_ai_content = __esm({
10448
10531
  fields = entryTypeConfig?.schema || [];
10449
10532
  }
10450
10533
  if (format === "json") {
10451
- const data = asRecord2(JSON.parse(raw));
10534
+ const data = asRecord(JSON.parse(raw));
10452
10535
  doc = {
10453
10536
  collection: schemaItem.logicalPath,
10454
10537
  collectionName: schemaItem.name,
@@ -10458,7 +10541,7 @@ var init_generate_ai_content = __esm({
10458
10541
  absolutePath
10459
10542
  };
10460
10543
  } else if (format === "yaml") {
10461
- const data = asRecord2(yamlParse(raw));
10544
+ const data = asRecord(yamlParse(raw));
10462
10545
  doc = {
10463
10546
  collection: schemaItem.logicalPath,
10464
10547
  collectionName: schemaItem.name,
@@ -11061,10 +11144,11 @@ var init_generate_ai_content = __esm({
11061
11144
  * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
11062
11145
  * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
11063
11146
  * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
11064
- * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
11065
- * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
11066
- * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
11067
- * cloned once per referencing entry.
11147
+ * EFS/NFS this targets the syscall the memo removes dominates both. A target resolves
11148
+ * without its body (an md/mdx target to its frontmatter, a json/yaml one without its `isBody`
11149
+ * field) unless the field sets `includeBody`, which is why `includeBody` is opt-in per
11150
+ * field: an embedded body sits inside the memoized object and is cloned once per referencing
11151
+ * entry.
11068
11152
  */
11069
11153
  resolveSingleReference(id, idIndex, includeBody, cache, access) {
11070
11154
  if (!cache)
@@ -11159,7 +11243,8 @@ var init_generate_ai_content = __esm({
11159
11243
  }));
11160
11244
  }
11161
11245
  const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
11162
- return buildResolvedReference(doc.data, meta, bodyForEmbed);
11246
+ const data = includeBody ? doc.data : linkOnlyReferenceData(doc.data, doc.format, this.entryTypeOf(location.collection, location.relativePath)?.schema);
11247
+ return buildResolvedReference(data, meta, bodyForEmbed);
11163
11248
  } catch (error) {
11164
11249
  if (isNodeError3(error) && error.code === "ENOENT")
11165
11250
  return STALE_LOOKUP;
@@ -13182,10 +13267,10 @@ var init_generate_ai_content = __esm({
13182
13267
  try {
13183
13268
  const raw = await fs20.readFile(filePath, "utf8");
13184
13269
  if (format === "json") {
13185
- return asRecord2(JSON.parse(raw));
13270
+ return asRecord(JSON.parse(raw));
13186
13271
  }
13187
13272
  if (format === "yaml") {
13188
- return asRecord2(yamlParse2(raw));
13273
+ return asRecord(yamlParse2(raw));
13189
13274
  }
13190
13275
  const parsed = matter3(raw);
13191
13276
  const data = { ...parsed.data ?? {} };
@@ -15305,7 +15390,7 @@ var init_mdx_allowlist3 = __esm({
15305
15390
 
15306
15391
  // dist/config/schemas/field.js
15307
15392
  import { z as z10 } from "zod";
15308
- 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;
15309
15394
  var init_field3 = __esm({
15310
15395
  "dist/config/schemas/field.js"() {
15311
15396
  "use strict";
@@ -15318,7 +15403,10 @@ var init_field3 = __esm({
15318
15403
  required: z10.boolean().optional(),
15319
15404
  list: z10.boolean().optional(),
15320
15405
  isTitle: z10.boolean().optional(),
15321
- 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()
15322
15410
  });
15323
15411
  selectOptionSchema3 = z10.union([
15324
15412
  z10.string(),
@@ -15376,6 +15464,7 @@ var init_field3 = __esm({
15376
15464
  type: z10.literal("select"),
15377
15465
  options: z10.array(selectOptionSchema3).min(1)
15378
15466
  });
15467
+ referenceSortFieldSchema3 = z10.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
15379
15468
  referenceFieldSchema3 = fieldBaseSchema3.extend({
15380
15469
  type: z10.literal("reference"),
15381
15470
  collections: z10.array(z10.string().min(1)).min(1).optional(),
@@ -15386,7 +15475,14 @@ var init_field3 = __esm({
15386
15475
  // default, so a consumer that adopts this schema's parse output would silently delete a
15387
15476
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
15388
15477
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
15389
- 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()
15390
15486
  });
15391
15487
  fieldHolder3 = [z10.never()];
15392
15488
  blockSchema3 = z10.object({
@@ -15620,7 +15716,7 @@ var init_deployment_name2 = __esm({
15620
15716
 
15621
15717
  // dist/config/schemas/config.js
15622
15718
  import { z as z14 } from "zod";
15623
- 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;
15624
15720
  var init_config4 = __esm({
15625
15721
  "dist/config/schemas/config.js"() {
15626
15722
  "use strict";
@@ -15667,6 +15763,19 @@ var init_config4 = __esm({
15667
15763
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
15668
15764
  SignInComponent: z14.custom().optional()
15669
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();
15670
15779
  CanopyConfigSchema3 = z14.object({
15671
15780
  media: mediaSchema3.optional(),
15672
15781
  // No outer .optional(): both schemas already .default('deny'), so the field is optional on
@@ -15716,6 +15825,7 @@ var init_config4 = __esm({
15716
15825
  entryLinkUrl: z14.custom().optional(),
15717
15826
  validateEntry: z14.custom().optional(),
15718
15827
  mdxAllow: mdxAllowlistSchema3.optional(),
15828
+ seo: seoSchema3.optional(),
15719
15829
  dev: devConfigSchema3.optional()
15720
15830
  }).strict();
15721
15831
  DEFAULT_PROD_WORKSPACE3 = "/mnt/efs";