canopycms 0.4.0 → 0.6.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 +78 -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 +136 -39
  11. package/dist/cli/generate-ai-content.js +75 -32
  12. package/dist/cli/init.js +34 -5
  13. package/dist/cli/template-files/cms-stack.ts.template +3 -0
  14. package/dist/cli/templates.js +5 -1
  15. package/dist/config/index.d.ts +1 -1
  16. package/dist/config/schemas/config.d.ts +74 -0
  17. package/dist/config/schemas/config.js +21 -0
  18. package/dist/config/schemas/field.d.ts +30 -0
  19. package/dist/config/schemas/field.js +22 -0
  20. package/dist/config/types.d.ts +39 -0
  21. package/dist/config/validation.d.ts +6 -0
  22. package/dist/config/validation.js +33 -0
  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 +5 -3
  55. package/dist/entry-schema.js +2 -1
  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/validation/entry-validator.d.ts +5 -0
  64. package/dist/validation/entry-validator.js +12 -0
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/dist/worker/github-gateway-http.js +2 -2
  68. package/dist/worker/github-gateway.js +2 -2
  69. package/package.json +1 -1
@@ -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;
package/dist/cli/init.js CHANGED
@@ -160,7 +160,7 @@ var init_mdx_allowlist = __esm({
160
160
 
161
161
  // dist/config/schemas/field.js
162
162
  import { z } from "zod";
163
- var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
163
+ var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
164
164
  var init_field = __esm({
165
165
  "dist/config/schemas/field.js"() {
166
166
  "use strict";
@@ -173,7 +173,10 @@ var init_field = __esm({
173
173
  required: z.boolean().optional(),
174
174
  list: z.boolean().optional(),
175
175
  isTitle: z.boolean().optional(),
176
- isBody: z.boolean().optional()
176
+ isBody: z.boolean().optional(),
177
+ // Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
178
+ // `ensureMaxItemsPlacement`'s call (config/validation.ts).
179
+ maxItems: z.number().int().positive().optional()
177
180
  });
178
181
  selectOptionSchema = z.union([
179
182
  z.string(),
@@ -231,6 +234,7 @@ var init_field = __esm({
231
234
  type: z.literal("select"),
232
235
  options: z.array(selectOptionSchema).min(1)
233
236
  });
237
+ referenceSortFieldSchema = z.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
234
238
  referenceFieldSchema = fieldBaseSchema.extend({
235
239
  type: z.literal("reference"),
236
240
  collections: z.array(z.string().min(1)).min(1).optional(),
@@ -241,7 +245,14 @@ var init_field = __esm({
241
245
  // default, so a consumer that adopts this schema's parse output would silently delete a
242
246
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
243
247
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
244
- includeBody: z.boolean().optional()
248
+ includeBody: z.boolean().optional(),
249
+ optionSort: z.union([
250
+ z.enum(["label", "collection"]),
251
+ z.object({
252
+ field: referenceSortFieldSchema,
253
+ direction: z.enum(["asc", "desc"]).optional()
254
+ }).strict()
255
+ ]).optional()
245
256
  });
246
257
  fieldHolder = [z.never()];
247
258
  blockSchema = z.object({
@@ -439,7 +450,7 @@ var init_media = __esm({
439
450
 
440
451
  // dist/config/schemas/config.js
441
452
  import { z as z5 } from "zod";
442
- var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
453
+ 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;
443
454
  var init_config = __esm({
444
455
  "dist/config/schemas/config.js"() {
445
456
  "use strict";
@@ -486,6 +497,19 @@ var init_config = __esm({
486
497
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
487
498
  SignInComponent: z5.custom().optional()
488
499
  });
500
+ seoFieldNameSchema = z5.string().min(1).optional();
501
+ seoSchema = z5.object({
502
+ group: z5.string().min(1).optional(),
503
+ fields: z5.object({
504
+ title: seoFieldNameSchema,
505
+ description: seoFieldNameSchema,
506
+ ogImage: seoFieldNameSchema,
507
+ ogType: seoFieldNameSchema,
508
+ canonical: seoFieldNameSchema,
509
+ noindex: seoFieldNameSchema,
510
+ twitterCard: seoFieldNameSchema
511
+ }).strict().optional()
512
+ }).strict();
489
513
  CanopyConfigSchema = z5.object({
490
514
  media: mediaSchema.optional(),
491
515
  // No outer .optional(): both schemas already .default('deny'), so the field is optional on
@@ -535,6 +559,7 @@ var init_config = __esm({
535
559
  entryLinkUrl: z5.custom().optional(),
536
560
  validateEntry: z5.custom().optional(),
537
561
  mdxAllow: mdxAllowlistSchema.optional(),
562
+ seo: seoSchema.optional(),
538
563
  dev: devConfigSchema.optional()
539
564
  }).strict();
540
565
  DEFAULT_PROD_WORKSPACE = "/mnt/efs";
@@ -632,9 +657,13 @@ async function canopyContext(options) {
632
657
  "// authenticated request instead of silently falling back to unauthenticated dev auth.",
633
658
  "// CLERK_SECRET_KEY is read only where Clerk's backend API is called: the auth-cache",
634
659
  "// refresh (the worker in prod, lazily on the dev server in dev).",
660
+ "// useOrganizationsAsGroups needs Organizations enabled on the Clerk instance and offers",
661
+ "// every organization in the instance as a group. In an AWS deployment the worker reads",
662
+ "// the setting from `clerkOrganizationsAsGroups` in the CDK stack, which governs there;",
663
+ "// keep the two equal.",
635
664
  "const authPlugin =",
636
665
  " config.server.mode === 'prod' || process.env.CANOPY_AUTH_MODE === 'clerk'",
637
- " ? createClerkAuthPlugin({ useOrganizationsAsGroups: true })",
666
+ " ? createClerkAuthPlugin({ useOrganizationsAsGroups: false })",
638
667
  " : createDevAuthPlugin()"
639
668
  ].join("\n") : "const authPlugin = createDevAuthPlugin()";
640
669
  return template.replace("{{AUTH_IMPORTS}}", authImports).replace("{{AUTH_PLUGIN_SETUP}}", authPluginSetup).replace("{{CONFIG_IMPORT}}", options.configImport);
@@ -172,6 +172,9 @@ export class CmsStack extends Stack {
172
172
  ].filter((arn): arn is string => arn !== undefined),
173
173
  githubTokenSecretArn: githubToken?.secretArn,
174
174
  clerkSecretKeySecretArn: clerkSecretKey.secretArn,
175
+ // Whether the worker maps Clerk Organizations onto Canopy groups. It needs Organizations
176
+ // enabled on the Clerk instance; keep it equal to useOrganizationsAsGroups in canopy.ts.
177
+ clerkOrganizationsAsGroups: false,
175
178
  // GitHub App auth, when configured instead of the token above. Undefined
176
179
  // unless the adopter set all three; CanopyCmsService refuses a partial
177
180
  // set, and refuses an App alongside githubTokenSecretArn.
@@ -41,9 +41,13 @@ export async function canopyContext(options) {
41
41
  '// authenticated request instead of silently falling back to unauthenticated dev auth.',
42
42
  "// CLERK_SECRET_KEY is read only where Clerk's backend API is called: the auth-cache",
43
43
  '// refresh (the worker in prod, lazily on the dev server in dev).',
44
+ '// useOrganizationsAsGroups needs Organizations enabled on the Clerk instance and offers',
45
+ '// every organization in the instance as a group. In an AWS deployment the worker reads',
46
+ '// the setting from `clerkOrganizationsAsGroups` in the CDK stack, which governs there;',
47
+ '// keep the two equal.',
44
48
  'const authPlugin =',
45
49
  " config.server.mode === 'prod' || process.env.CANOPY_AUTH_MODE === 'clerk'",
46
- ' ? createClerkAuthPlugin({ useOrganizationsAsGroups: true })',
50
+ ' ? createClerkAuthPlugin({ useOrganizationsAsGroups: false })',
47
51
  ' : createDevAuthPlugin()',
48
52
  ].join('\n')
49
53
  : 'const authPlugin = createDevAuthPlugin()';
@@ -8,7 +8,7 @@
8
8
  * import { defineCanopyConfig, type FieldConfig } from 'canopycms'
9
9
  * ```
10
10
  */
11
- export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
11
+ export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ReferenceOptionSort, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
12
12
  export { primitiveFieldTypes, fieldTypes } from './types.js';
13
13
  export { CanopyConfigSchema, getConfigDefaults, DEFAULT_PROD_WORKSPACE } from './schemas/config.js';
14
14
  export { fieldSchema, blockSchema, selectOptionSchema, referenceOptionSchema, imageFieldSchema, } from './schemas/field.js';
@@ -105,6 +105,56 @@ export declare const CanopyConfigSchema: z.ZodObject<{
105
105
  entryLinkUrl: z.ZodOptional<z.ZodType<EntryLinkUrlResolver, z.ZodTypeDef, EntryLinkUrlResolver>>;
106
106
  validateEntry: z.ZodOptional<z.ZodType<ValidateEntryHook, z.ZodTypeDef, ValidateEntryHook>>;
107
107
  mdxAllow: z.ZodOptional<z.ZodType<import("../index.js").MdxAllowlist, z.ZodTypeDef, import("../index.js").MdxAllowlist>>;
108
+ seo: z.ZodOptional<z.ZodObject<{
109
+ group: z.ZodOptional<z.ZodString>;
110
+ fields: z.ZodOptional<z.ZodObject<{
111
+ title: z.ZodOptional<z.ZodString>;
112
+ description: z.ZodOptional<z.ZodString>;
113
+ ogImage: z.ZodOptional<z.ZodString>;
114
+ ogType: z.ZodOptional<z.ZodString>;
115
+ canonical: z.ZodOptional<z.ZodString>;
116
+ noindex: z.ZodOptional<z.ZodString>;
117
+ twitterCard: z.ZodOptional<z.ZodString>;
118
+ }, "strict", z.ZodTypeAny, {
119
+ description?: string | undefined;
120
+ title?: string | undefined;
121
+ ogImage?: string | undefined;
122
+ ogType?: string | undefined;
123
+ canonical?: string | undefined;
124
+ noindex?: string | undefined;
125
+ twitterCard?: string | undefined;
126
+ }, {
127
+ description?: string | undefined;
128
+ title?: string | undefined;
129
+ ogImage?: string | undefined;
130
+ ogType?: string | undefined;
131
+ canonical?: string | undefined;
132
+ noindex?: string | undefined;
133
+ twitterCard?: string | undefined;
134
+ }>>;
135
+ }, "strict", z.ZodTypeAny, {
136
+ fields?: {
137
+ description?: string | undefined;
138
+ title?: string | undefined;
139
+ ogImage?: string | undefined;
140
+ ogType?: string | undefined;
141
+ canonical?: string | undefined;
142
+ noindex?: string | undefined;
143
+ twitterCard?: string | undefined;
144
+ } | undefined;
145
+ group?: string | undefined;
146
+ }, {
147
+ fields?: {
148
+ description?: string | undefined;
149
+ title?: string | undefined;
150
+ ogImage?: string | undefined;
151
+ ogType?: string | undefined;
152
+ canonical?: string | undefined;
153
+ noindex?: string | undefined;
154
+ twitterCard?: string | undefined;
155
+ } | undefined;
156
+ group?: string | undefined;
157
+ }>>;
108
158
  dev: z.ZodOptional<z.ZodObject<{
109
159
  contentSync: z.ZodOptional<z.ZodDefault<z.ZodEnum<["off", "warn"]>>>;
110
160
  }, "strip", z.ZodTypeAny, {
@@ -167,6 +217,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
167
217
  entryLinkUrl?: EntryLinkUrlResolver | undefined;
168
218
  validateEntry?: ValidateEntryHook | undefined;
169
219
  mdxAllow?: import("../index.js").MdxAllowlist | undefined;
220
+ seo?: {
221
+ fields?: {
222
+ description?: string | undefined;
223
+ title?: string | undefined;
224
+ ogImage?: string | undefined;
225
+ ogType?: string | undefined;
226
+ canonical?: string | undefined;
227
+ noindex?: string | undefined;
228
+ twitterCard?: string | undefined;
229
+ } | undefined;
230
+ group?: string | undefined;
231
+ } | undefined;
170
232
  }, {
171
233
  gitBotAuthorName: string;
172
234
  gitBotAuthorEmail: string;
@@ -222,6 +284,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
222
284
  entryLinkUrl?: EntryLinkUrlResolver | undefined;
223
285
  validateEntry?: ValidateEntryHook | undefined;
224
286
  mdxAllow?: import("../index.js").MdxAllowlist | undefined;
287
+ seo?: {
288
+ fields?: {
289
+ description?: string | undefined;
290
+ title?: string | undefined;
291
+ ogImage?: string | undefined;
292
+ ogType?: string | undefined;
293
+ canonical?: string | undefined;
294
+ noindex?: string | undefined;
295
+ twitterCard?: string | undefined;
296
+ } | undefined;
297
+ group?: string | undefined;
298
+ } | undefined;
225
299
  }>;
226
300
  /**
227
301
  * Default workspace path for prod mode (used when CANOPYCMS_WORKSPACE_ROOT is not set): where
@@ -61,6 +61,26 @@ const editorConfigSchema = z.object({
61
61
  // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
62
62
  SignInComponent: z.custom().optional(),
63
63
  });
64
+ const seoFieldNameSchema = z.string().min(1).optional();
65
+ // Strict, so a misspelled key (`noIndex`) fails the parse instead of leaving every surface
66
+ // reading the default field name.
67
+ const seoSchema = z
68
+ .object({
69
+ group: z.string().min(1).optional(),
70
+ fields: z
71
+ .object({
72
+ title: seoFieldNameSchema,
73
+ description: seoFieldNameSchema,
74
+ ogImage: seoFieldNameSchema,
75
+ ogType: seoFieldNameSchema,
76
+ canonical: seoFieldNameSchema,
77
+ noindex: seoFieldNameSchema,
78
+ twitterCard: seoFieldNameSchema,
79
+ })
80
+ .strict()
81
+ .optional(),
82
+ })
83
+ .strict();
64
84
  export const CanopyConfigSchema = z
65
85
  .object({
66
86
  media: mediaSchema.optional(),
@@ -110,6 +130,7 @@ export const CanopyConfigSchema = z
110
130
  entryLinkUrl: z.custom().optional(),
111
131
  validateEntry: z.custom().optional(),
112
132
  mdxAllow: mdxAllowlistSchema.optional(),
133
+ seo: seoSchema.optional(),
113
134
  dev: devConfigSchema.optional(),
114
135
  })
115
136
  .strict();
@@ -33,6 +33,7 @@ export declare const imageFieldSchema: z.ZodObject<{
33
33
  list: z.ZodOptional<z.ZodBoolean>;
34
34
  isTitle: z.ZodOptional<z.ZodBoolean>;
35
35
  isBody: z.ZodOptional<z.ZodBoolean>;
36
+ maxItems: z.ZodOptional<z.ZodNumber>;
36
37
  } & {
37
38
  type: z.ZodLiteral<"image">;
38
39
  aspect: z.ZodOptional<z.ZodString>;
@@ -46,6 +47,7 @@ export declare const imageFieldSchema: z.ZodObject<{
46
47
  list?: boolean | undefined;
47
48
  isTitle?: boolean | undefined;
48
49
  isBody?: boolean | undefined;
50
+ maxItems?: number | undefined;
49
51
  aspect?: string | undefined;
50
52
  altOptional?: boolean | undefined;
51
53
  }, {
@@ -57,9 +59,16 @@ export declare const imageFieldSchema: z.ZodObject<{
57
59
  list?: boolean | undefined;
58
60
  isTitle?: boolean | undefined;
59
61
  isBody?: boolean | undefined;
62
+ maxItems?: number | undefined;
60
63
  aspect?: string | undefined;
61
64
  altOptional?: boolean | undefined;
62
65
  }>;
66
+ /**
67
+ * A dotted field path for `optionSort: { field }`: non-empty segments, so `'a..b'` and `'.a'`
68
+ * fail at config time rather than sorting on a value no entry has.
69
+ * @internal Exported for the reference-options endpoint, which validates `sortField` the same way.
70
+ */
71
+ export declare const referenceSortFieldSchema: z.ZodString;
63
72
  /** @internal Exported for tests. */
64
73
  export declare const referenceFieldSchema: z.ZodObject<{
65
74
  name: z.ZodString;
@@ -69,6 +78,7 @@ export declare const referenceFieldSchema: z.ZodObject<{
69
78
  list: z.ZodOptional<z.ZodBoolean>;
70
79
  isTitle: z.ZodOptional<z.ZodBoolean>;
71
80
  isBody: z.ZodOptional<z.ZodBoolean>;
81
+ maxItems: z.ZodOptional<z.ZodNumber>;
72
82
  } & {
73
83
  type: z.ZodLiteral<"reference">;
74
84
  collections: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -85,6 +95,16 @@ export declare const referenceFieldSchema: z.ZodObject<{
85
95
  label: string;
86
96
  }>]>, "many">>;
87
97
  includeBody: z.ZodOptional<z.ZodBoolean>;
98
+ optionSort: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["label", "collection"]>, z.ZodObject<{
99
+ field: z.ZodString;
100
+ direction: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
101
+ }, "strict", z.ZodTypeAny, {
102
+ field: string;
103
+ direction?: "asc" | "desc" | undefined;
104
+ }, {
105
+ field: string;
106
+ direction?: "asc" | "desc" | undefined;
107
+ }>]>>;
88
108
  }, "strip", z.ZodTypeAny, {
89
109
  name: string;
90
110
  type: "reference";
@@ -98,10 +118,15 @@ export declare const referenceFieldSchema: z.ZodObject<{
98
118
  list?: boolean | undefined;
99
119
  isTitle?: boolean | undefined;
100
120
  isBody?: boolean | undefined;
121
+ maxItems?: number | undefined;
101
122
  collections?: string[] | undefined;
102
123
  entryTypes?: string[] | undefined;
103
124
  displayField?: string | undefined;
104
125
  includeBody?: boolean | undefined;
126
+ optionSort?: "collection" | "label" | {
127
+ field: string;
128
+ direction?: "asc" | "desc" | undefined;
129
+ } | undefined;
105
130
  }, {
106
131
  name: string;
107
132
  type: "reference";
@@ -115,10 +140,15 @@ export declare const referenceFieldSchema: z.ZodObject<{
115
140
  list?: boolean | undefined;
116
141
  isTitle?: boolean | undefined;
117
142
  isBody?: boolean | undefined;
143
+ maxItems?: number | undefined;
118
144
  collections?: string[] | undefined;
119
145
  entryTypes?: string[] | undefined;
120
146
  displayField?: string | undefined;
121
147
  includeBody?: boolean | undefined;
148
+ optionSort?: "collection" | "label" | {
149
+ field: string;
150
+ direction?: "asc" | "desc" | undefined;
151
+ } | undefined;
122
152
  }>;
123
153
  export declare const blockSchema: z.ZodObject<{
124
154
  name: z.ZodString;
@@ -12,6 +12,9 @@ const fieldBaseSchema = z.object({
12
12
  list: z.boolean().optional(),
13
13
  isTitle: z.boolean().optional(),
14
14
  isBody: z.boolean().optional(),
15
+ // Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
16
+ // `ensureMaxItemsPlacement`'s call (config/validation.ts).
17
+ maxItems: z.number().int().positive().optional(),
15
18
  });
16
19
  export const selectOptionSchema = z.union([
17
20
  z.string(),
@@ -94,6 +97,14 @@ const selectFieldSchema = fieldBaseSchema.extend({
94
97
  type: z.literal('select'),
95
98
  options: z.array(selectOptionSchema).min(1),
96
99
  });
100
+ /**
101
+ * A dotted field path for `optionSort: { field }`: non-empty segments, so `'a..b'` and `'.a'`
102
+ * fail at config time rather than sorting on a value no entry has.
103
+ * @internal Exported for the reference-options endpoint, which validates `sortField` the same way.
104
+ */
105
+ export const referenceSortFieldSchema = z
106
+ .string()
107
+ .regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
97
108
  // At least one of `collections` or `entryTypes` must be specified (enforced by config validation).
98
109
  /** @internal Exported for tests. */
99
110
  export const referenceFieldSchema = fieldBaseSchema.extend({
@@ -107,6 +118,17 @@ export const referenceFieldSchema = fieldBaseSchema.extend({
107
118
  // runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
108
119
  // absent on purpose — it is type-inference-only and stripping it changes no behavior.)
109
120
  includeBody: z.boolean().optional(),
121
+ optionSort: z
122
+ .union([
123
+ z.enum(['label', 'collection']),
124
+ z
125
+ .object({
126
+ field: referenceSortFieldSchema,
127
+ direction: z.enum(['asc', 'desc']).optional(),
128
+ })
129
+ .strict(),
130
+ ])
131
+ .optional(),
110
132
  });
111
133
  // Use a mutable holder to enable forward references in recursive z.lazy() closures.
112
134
  // blockSchema/objectFieldSchema reference fieldHolder[0] via z.lazy, which is resolved