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
@@ -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";
@@ -2912,13 +2937,36 @@ function flattenGroupFields(fields) {
2912
2937
  return result;
2913
2938
  }
2914
2939
 
2940
+ // dist/utils/format.js
2941
+ var getFormatExtension = (format) => {
2942
+ if (format === "md")
2943
+ return ".md";
2944
+ if (format === "mdx")
2945
+ return ".mdx";
2946
+ if (format === "yaml")
2947
+ return ".yaml";
2948
+ return ".json";
2949
+ };
2950
+ var isDataOnlyFormat = (format) => format === "json" || format === "yaml";
2951
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
2952
+
2915
2953
  // dist/utils/body-field.js
2916
2954
  function findBodyFieldName(fields) {
2955
+ return findExplicitBodyFieldName(fields) ?? "body";
2956
+ }
2957
+ function findExplicitBodyFieldName(fields) {
2917
2958
  for (const field of flattenGroupFields(fields)) {
2918
2959
  if ("isBody" in field && field.isBody)
2919
2960
  return field.name;
2920
2961
  }
2921
- return "body";
2962
+ return void 0;
2963
+ }
2964
+ function linkOnlyReferenceData(data, format, fields) {
2965
+ const bodyFieldName = isDataOnlyFormat(format) && fields ? findExplicitBodyFieldName(fields) : void 0;
2966
+ if (bodyFieldName === void 0 || !(bodyFieldName in data))
2967
+ return data;
2968
+ const { [bodyFieldName]: _body, ...rest } = data;
2969
+ return rest;
2922
2970
  }
2923
2971
 
2924
2972
  // dist/content-store.js
@@ -2934,7 +2982,7 @@ var DEFAULT_SEO_FIELD_NAMES = {
2934
2982
  noindex: "noindex",
2935
2983
  twitterCard: "twitterCard"
2936
2984
  };
2937
- var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2985
+ var asRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2938
2986
  var asText = (value) => {
2939
2987
  if (typeof value !== "string")
2940
2988
  return void 0;
@@ -2946,7 +2994,7 @@ var asImageUrl = (value) => {
2946
2994
  const text = asText(value);
2947
2995
  if (text)
2948
2996
  return text;
2949
- return asText(asRecord(value)?.src);
2997
+ return asText(asRecord2(value)?.src);
2950
2998
  };
2951
2999
  var asOgType = (value) => {
2952
3000
  const text = asText(value);
@@ -2957,9 +3005,13 @@ var asTwitterCard = (value) => {
2957
3005
  return text === "summary" || text === "summary_large_image" ? text : void 0;
2958
3006
  };
2959
3007
  function extractSeoFields(entryData, opts = {}) {
2960
- const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
2961
- const entry = asRecord(entryData);
2962
- const source = opts.group ? asRecord(entry?.[opts.group]) : entry;
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
+ }
3013
+ const entry = asRecord2(entryData);
3014
+ const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
2963
3015
  return {
2964
3016
  title: asText(source?.[names.title]) ?? asText(opts.fallbackTitle),
2965
3017
  description: asText(source?.[names.description]) ?? asText(opts.fallbackDescription),
@@ -4459,19 +4511,6 @@ async function readFileWithMtimeIfExists(filePath) {
4459
4511
  }
4460
4512
  }
4461
4513
 
4462
- // dist/utils/format.js
4463
- var getFormatExtension = (format) => {
4464
- if (format === "md")
4465
- return ".md";
4466
- if (format === "mdx")
4467
- return ".mdx";
4468
- if (format === "yaml")
4469
- return ".yaml";
4470
- return ".json";
4471
- };
4472
- var isDataOnlyFormat = (format) => format === "json" || format === "yaml";
4473
- var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
4474
-
4475
4514
  // dist/paths/index.js
4476
4515
  init_normalize();
4477
4516
 
@@ -5085,7 +5124,7 @@ var ContentStore = class {
5085
5124
  fields = entryTypeConfig?.schema || [];
5086
5125
  }
5087
5126
  if (format === "json") {
5088
- const data = asRecord2(JSON.parse(raw));
5127
+ const data = asRecord(JSON.parse(raw));
5089
5128
  doc = {
5090
5129
  collection: schemaItem.logicalPath,
5091
5130
  collectionName: schemaItem.name,
@@ -5095,7 +5134,7 @@ var ContentStore = class {
5095
5134
  absolutePath
5096
5135
  };
5097
5136
  } else if (format === "yaml") {
5098
- const data = asRecord2(yamlParse(raw));
5137
+ const data = asRecord(yamlParse(raw));
5099
5138
  doc = {
5100
5139
  collection: schemaItem.logicalPath,
5101
5140
  collectionName: schemaItem.name,
@@ -5698,10 +5737,11 @@ var ContentStore = class {
5698
5737
  * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
5699
5738
  * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
5700
5739
  * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
5701
- * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
5702
- * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
5703
- * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
5704
- * cloned once per referencing entry.
5740
+ * EFS/NFS this targets the syscall the memo removes dominates both. A target resolves
5741
+ * without its body (an md/mdx target to its frontmatter, a json/yaml one without its `isBody`
5742
+ * field) unless the field sets `includeBody`, which is why `includeBody` is opt-in per
5743
+ * field: an embedded body sits inside the memoized object and is cloned once per referencing
5744
+ * entry.
5705
5745
  */
5706
5746
  resolveSingleReference(id, idIndex, includeBody, cache, access) {
5707
5747
  if (!cache)
@@ -5796,7 +5836,8 @@ var ContentStore = class {
5796
5836
  }));
5797
5837
  }
5798
5838
  const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
5799
- return buildResolvedReference(doc.data, meta, bodyForEmbed);
5839
+ const data = includeBody ? doc.data : linkOnlyReferenceData(doc.data, doc.format, this.entryTypeOf(location.collection, location.relativePath)?.schema);
5840
+ return buildResolvedReference(data, meta, bodyForEmbed);
5800
5841
  } catch (error) {
5801
5842
  if (isNodeError(error) && error.code === "ENOENT")
5802
5843
  return STALE_LOOKUP;
@@ -6711,7 +6752,7 @@ function createReferenceRendering(idIndex, flatSchema, entryLinkUrl, markdownCop
6711
6752
 
6712
6753
  // dist/ai/generate.js
6713
6754
  async function generateAIContent(options) {
6714
- const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
6755
+ const { store, flatSchema, contentRoot, config, entryLinkUrl, seo, generatedAt, buildId } = options;
6715
6756
  assertValidNoindexConfig(config);
6716
6757
  assertValidReferenceLink(config);
6717
6758
  const files = /* @__PURE__ */ new Map();
@@ -6722,6 +6763,7 @@ async function generateAIContent(options) {
6722
6763
  config,
6723
6764
  idIndex,
6724
6765
  entryLinkUrl,
6766
+ seo,
6725
6767
  resolveTarget: createReferenceTargetResolver(store)
6726
6768
  };
6727
6769
  const collections = flatSchema.filter((item) => item.type === "collection");
@@ -6752,14 +6794,18 @@ async function generateAIContent(options) {
6752
6794
  const pending = pendingById.get(id);
6753
6795
  if (!pending || !exported.has(id))
6754
6796
  return void 0;
6755
- const { slug } = pending.entry;
6756
- const target = buildResolvedReference(structuredClone(pending.storedData), {
6797
+ const { format } = pending;
6798
+ const { slug, fields } = pending.entry;
6799
+ const storedData = structuredClone(pending.storedData);
6800
+ const target = buildResolvedReference(linkOnlyReferenceData(storedData, format, fields), {
6757
6801
  id,
6758
6802
  slug,
6759
6803
  collection: pending.collectionPath,
6760
6804
  urlPath: computeEntryUrl(pending.collectionPath, slug, contentRoot)
6761
6805
  });
6762
- return { target, body: pending.body };
6806
+ const dataBodyField = isDataOnlyFormat(format) ? findExplicitBodyFieldName(fields) : void 0;
6807
+ const dataBody = dataBodyField === void 0 ? void 0 : storedData[dataBodyField];
6808
+ return { target, body: pending.body ?? (typeof dataBody === "string" ? dataBody : void 0) };
6763
6809
  };
6764
6810
  const linkFor = config?.referenceLink ? await decideReferenceLinks(config.referenceLink, allPending, subjectOf, entryLinkUrl) : void 0;
6765
6811
  const bodyLinkUrl = linkFor ? (location) => {
@@ -6869,7 +6915,7 @@ async function collectCollection(read, collection, flatSchema) {
6869
6915
  return { collection, cleanPath, entries, subcollections };
6870
6916
  }
6871
6917
  async function collectEntries(read, collection, cleanPath) {
6872
- const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget } = read;
6918
+ const { store, contentRoot, config, idIndex, entryLinkUrl, seo, resolveTarget } = read;
6873
6919
  const skipNoindex = config?.exclude?.noindex !== false;
6874
6920
  const pending = [];
6875
6921
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
@@ -6887,7 +6933,7 @@ async function collectEntries(read, collection, cleanPath) {
6887
6933
  const doc = await store.read(listEntry.collection, listEntry.slug, {
6888
6934
  resolveReferences: false
6889
6935
  });
6890
- if (skipNoindex && isNoindexEntry(doc.data, config?.seo))
6936
+ if (skipNoindex && isNoindexEntry(doc.data, seo))
6891
6937
  continue;
6892
6938
  const storedData = doc.data;
6893
6939
  doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget);
@@ -6901,6 +6947,7 @@ async function collectEntries(read, collection, cleanPath) {
6901
6947
  entry: aiEntry,
6902
6948
  body,
6903
6949
  storedData,
6950
+ format: doc.format,
6904
6951
  linkedIds: [],
6905
6952
  contentId: extractIdFromFilename(path13.basename(listEntry.relativePath)),
6906
6953
  absolutePath: doc.absolutePath,
@@ -6953,24 +7000,8 @@ function assertValidNoindexConfig(config) {
6953
7000
  if (noindex !== void 0 && typeof noindex !== "boolean") {
6954
7001
  throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
6955
7002
  }
6956
- const seo = config?.seo;
6957
- if (seo === void 0)
6958
- return;
6959
- if (typeof seo !== "object" || seo === null || Array.isArray(seo)) {
6960
- throw new Error("AI content config: seo must be an object of { group?, fields? }");
6961
- }
6962
- const { group, fields } = seo;
6963
- if (group !== void 0 && typeof group !== "string") {
6964
- throw new Error("AI content config: seo.group must be a string");
6965
- }
6966
- if (fields === void 0)
6967
- return;
6968
- if (typeof fields !== "object" || fields === null || Array.isArray(fields) || Object.values(fields).some((name) => typeof name !== "string")) {
6969
- throw new Error("AI content config: seo.fields must map SEO fields to field-name strings");
6970
- }
6971
- const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
6972
- if (unknown.length > 0) {
6973
- 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.");
6974
7005
  }
6975
7006
  }
6976
7007
  function stripContentRoot(logicalPath, contentRoot) {
@@ -9794,10 +9825,10 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
9794
9825
  try {
9795
9826
  const raw = await fs20.readFile(filePath, "utf8");
9796
9827
  if (format === "json") {
9797
- return asRecord2(JSON.parse(raw));
9828
+ return asRecord(JSON.parse(raw));
9798
9829
  }
9799
9830
  if (format === "yaml") {
9800
- return asRecord2(yamlParse2(raw));
9831
+ return asRecord(yamlParse2(raw));
9801
9832
  }
9802
9833
  const parsed = matter3(raw);
9803
9834
  const data = { ...parsed.data ?? {} };
@@ -10121,6 +10152,14 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
10121
10152
  }
10122
10153
  if (value === void 0 || value === null)
10123
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
+ }
10124
10163
  if (field.type === "block") {
10125
10164
  const blockField = field;
10126
10165
  if (!Array.isArray(value)) {
@@ -10221,14 +10260,34 @@ function assertBuildEntriesValid(items, phaseLabel) {
10221
10260
  const invalid = findInvalidEntries(items);
10222
10261
  if (invalid.length === 0)
10223
10262
  return;
10224
- const lines = invalid.map(({ entryPath, errors }) => {
10225
- const [first, ...rest] = errors;
10226
- const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
10227
- return ` - ${entryPath} \u2014 ${summary}`;
10228
- });
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
+ }
10229
10288
  throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
10230
10289
  ${lines.join("\n")}
10231
- 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"));
10232
10291
  }
10233
10292
 
10234
10293
  // dist/build/generate-ai-content.js
@@ -10325,6 +10384,7 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
10325
10384
  contentRoot: contentRootName,
10326
10385
  config: aiConfig,
10327
10386
  entryLinkUrl: config.entryLinkUrl,
10387
+ seo: config.seo,
10328
10388
  ...resolveBuildStamp()
10329
10389
  });
10330
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";
@@ -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();