canopycms 0.8.3 → 0.9.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.
package/dist/cli/cli.js CHANGED
@@ -1349,7 +1349,7 @@ var init_init = __esm({
1349
1349
  init_types = __esm2({
1350
1350
  "dist/config/types.js"() {
1351
1351
  "use strict";
1352
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
1352
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
1353
1353
  markdownFieldTypes = ["markdown", "mdx"];
1354
1354
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
1355
1355
  fieldTypes = [
@@ -3495,7 +3495,7 @@ async function atomicWriteFile2(filePath, content) {
3495
3495
  }
3496
3496
  }
3497
3497
  function isPlainRecord(value) {
3498
- return typeof value === "object" && value !== null && !Array.isArray(value);
3498
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
3499
3499
  }
3500
3500
  function resolveBlockItem(blockField, item) {
3501
3501
  const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
@@ -4068,6 +4068,20 @@ async function writeWorkerStatus(taskDir, report) {
4068
4068
  function isBlockStructuralKey(key) {
4069
4069
  return BLOCK_STRUCTURAL_KEYS.has(key);
4070
4070
  }
4071
+ function withCalendarDays(fields, data) {
4072
+ let out = data;
4073
+ for (const field of fields) {
4074
+ if (field.type === "group") {
4075
+ out = withCalendarDays(field.fields, out);
4076
+ continue;
4077
+ }
4078
+ const value = out[field.name];
4079
+ const next = valueWithDays(field, value);
4080
+ if (next !== value)
4081
+ out = { ...out, [field.name]: next };
4082
+ }
4083
+ return out;
4084
+ }
4071
4085
  function createDebugLogger2(options) {
4072
4086
  return new DebugLogger2(options);
4073
4087
  }
@@ -4711,9 +4725,9 @@ function asJson(value) {
4711
4725
  const key = identityKey(value);
4712
4726
  return key === void 0 || key === "undefined" ? void 0 : JSON.parse(key);
4713
4727
  }
4714
- function frontmatterAsRead(raw) {
4728
+ function frontmatterAsRead(raw, fields) {
4715
4729
  try {
4716
- return asJson(matter(raw, {}).data);
4730
+ return asJson(withCalendarDays(fields, matter(raw, {}).data));
4717
4731
  } catch {
4718
4732
  return void 0;
4719
4733
  }
@@ -4781,7 +4795,7 @@ ${asKey ? `${text3}: v` : `v: ${text3}`}
4781
4795
  return true;
4782
4796
  }
4783
4797
  }
4784
- function serializeFrontmatter(editorBody, data, existingRaw, format) {
4798
+ function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []) {
4785
4799
  const file = { content: bodyToWrite(editorBody, data, format, existingRaw) };
4786
4800
  if (existingRaw === void 0)
4787
4801
  return matter.stringify(file, data);
@@ -4789,7 +4803,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
4789
4803
  if (existingFrontmatter === void 0)
4790
4804
  return matter.stringify(file, data);
4791
4805
  const onDisk = parseDocument2(existingFrontmatter);
4792
- const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw), onDisk.toJS()) : data;
4806
+ const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw, fields), onDisk.toJS()) : data;
4793
4807
  const reconciled = reconcileYamlSource(existingFrontmatter, isPlainRecord2(toReconcile) ? toReconcile : data, { rootAtColumnZero: true, quote: readsAsAnotherValue });
4794
4808
  if (reconciled === void 0)
4795
4809
  return matter.stringify(file, data);
@@ -4801,7 +4815,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
4801
4815
  }
4802
4816
  }
4803
4817
  });
4804
- if (isDeepStrictEqual2(frontmatterAsRead(written), asJson(data)))
4818
+ if (isDeepStrictEqual2(frontmatterAsRead(written, fields), asJson(data)))
4805
4819
  return written;
4806
4820
  log22.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
4807
4821
  return matter.stringify(file, data);
@@ -6131,6 +6145,7 @@ _${field.description}_` : "";
6131
6145
  switch (field.type) {
6132
6146
  case "string":
6133
6147
  case "number":
6148
+ case "date":
6134
6149
  case "datetime":
6135
6150
  return `${heading} ${label}${descriptionLine}
6136
6151
 
@@ -8054,6 +8069,11 @@ function validateScalar(field, value, path272) {
8054
8069
  return [{ fieldPath: path272, message: "Expected true or false" }];
8055
8070
  }
8056
8071
  return [];
8072
+ case "date":
8073
+ if (value !== "" && !isCalendarDay(value)) {
8074
+ return [{ fieldPath: path272, message: "Expected a real date, written YYYY-MM-DD" }];
8075
+ }
8076
+ return [];
8057
8077
  case "datetime":
8058
8078
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
8059
8079
  return [{ fieldPath: path272, message: "Expected a valid date/time string" }];
@@ -8206,7 +8226,7 @@ function findInvalidEntries(items) {
8206
8226
  for (const item of items) {
8207
8227
  if (!item.schema)
8208
8228
  continue;
8209
- const data = normalizeDatesDeep(item.data);
8229
+ const data = normalizeDatesDeep(withCalendarDays(item.schema, item.data));
8210
8230
  const errors = validateEntryData(item.schema, data);
8211
8231
  if (errors.length > 0) {
8212
8232
  invalid.push({ entryPath: item.entryPath, errors });
@@ -8434,7 +8454,7 @@ async function generateAIContentCLI(options) {
8434
8454
  console.log(` Output: ${result.outputDir}
8435
8455
  `);
8436
8456
  }
8437
- 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, RESERVED_SETTINGS_BRANCH_PREFIX2, init_branch_name, BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2, MAX_BRANCH_NAME_LENGTH2, init_validation, 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, editorUrlSchema2, 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, warned22, init_mode_env, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, warned4, DEPLOYMENT_PLACEHOLDER2, DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, BRANCH_NAMESPACE_ENV2, init_branch_namespace, 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, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, 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, warned5, SchemaUnavailableError, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, 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, SNAPSHOT_MEMO_MAX_BYTES, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, 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, GitProcessError, STDERR_CAP_BYTES, KILL_GRACE_MS, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, HASH_INACTIVITY_TIMEOUT_MS, MAX_HASH_RUNS, LS_REMOTE_INACTIVITY_TIMEOUT_MS, symlinkBlobSha, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, BaseBranchMissingError, 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, AI_USER_ID, log10, normalizePath, parseEntryParts, parseEntryData, 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;
8457
+ 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, RESERVED_SETTINGS_BRANCH_PREFIX2, init_branch_name, BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2, MAX_BRANCH_NAME_LENGTH2, init_validation, 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, editorUrlSchema2, 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, warned22, init_mode_env, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, warned4, DEPLOYMENT_PLACEHOLDER2, DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, BRANCH_NAMESPACE_ENV2, init_branch_namespace, 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, DAY, MS_PER_DAY, isRealDay, dayParts, isCalendarDay, dayOfBareDate, mapItems, blockItemWithDays, valueWithDays, entryAsRead, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, 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, warned5, SchemaUnavailableError, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, 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, SNAPSHOT_MEMO_MAX_BYTES, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, 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, GitProcessError, STDERR_CAP_BYTES, KILL_GRACE_MS, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, HASH_INACTIVITY_TIMEOUT_MS, MAX_HASH_RUNS, LS_REMOTE_INACTIVITY_TIMEOUT_MS, symlinkBlobSha, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, BaseBranchMissingError, 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, AI_USER_ID, log10, normalizePath, parseEntryParts, parseEntryData, 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;
8438
8458
  var init_generate_ai_content = __esm({
8439
8459
  "dist/cli/generate-ai-content.js"() {
8440
8460
  "use strict";
@@ -8616,7 +8636,7 @@ var init_generate_ai_content = __esm({
8616
8636
  init_types22 = __esm3({
8617
8637
  "dist/config/types.js"() {
8618
8638
  "use strict";
8619
- scalarFieldTypes2 = ["string", "number", "boolean", "datetime", "code"];
8639
+ scalarFieldTypes2 = ["string", "number", "boolean", "date", "datetime", "code"];
8620
8640
  markdownFieldTypes2 = ["markdown", "mdx"];
8621
8641
  primitiveFieldTypes2 = [...scalarFieldTypes2, ...markdownFieldTypes2];
8622
8642
  fieldTypes2 = [
@@ -9350,6 +9370,54 @@ var init_generate_ai_content = __esm({
9350
9370
  BLOCK_DISCRIMINATOR_KEYS = ["template", "_type"];
9351
9371
  BLOCK_STRUCTURAL_KEYS = new Set(BLOCK_DISCRIMINATOR_KEYS);
9352
9372
  init_item_keys();
9373
+ init_item_keys();
9374
+ init_field_traversal();
9375
+ DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
9376
+ MS_PER_DAY = 864e5;
9377
+ isRealDay = (year, month, day) => {
9378
+ const lastOfMonth = /* @__PURE__ */ new Date(0);
9379
+ lastOfMonth.setUTCFullYear(year, month, 0);
9380
+ return month >= 1 && month <= 12 && day >= 1 && day <= lastOfMonth.getUTCDate();
9381
+ };
9382
+ dayParts = (value) => {
9383
+ const match = typeof value === "string" ? DAY.exec(value) : null;
9384
+ if (!match)
9385
+ return void 0;
9386
+ const [year, month, day] = match.slice(1).map(Number);
9387
+ return isRealDay(year, month, day) ? [year, month, day] : void 0;
9388
+ };
9389
+ isCalendarDay = (value) => dayParts(value) !== void 0;
9390
+ dayOfBareDate = (value) => value.getTime() % MS_PER_DAY === 0 ? value.toISOString().slice(0, 10) : void 0;
9391
+ mapItems = (items, mapItem) => {
9392
+ const next = items.map((item) => isPlainRecord(item) ? mapItem(item) : item);
9393
+ return next.every((item, i) => item === items[i]) ? items : next;
9394
+ };
9395
+ blockItemWithDays = (field, item) => {
9396
+ const resolved = resolveBlockItem(field, item);
9397
+ if (!resolved)
9398
+ return item;
9399
+ const data = withCalendarDays(resolved.fields, resolved.data);
9400
+ if (data === resolved.data)
9401
+ return item;
9402
+ return resolved.data === item ? data : { ...item, value: data };
9403
+ };
9404
+ valueWithDays = (field, value) => {
9405
+ if (field.type === "date") {
9406
+ return value instanceof Date ? dayOfBareDate(value) ?? value : value;
9407
+ }
9408
+ if (field.type === "object") {
9409
+ const { fields, list } = field;
9410
+ if (list) {
9411
+ return Array.isArray(value) ? mapItems(value, (item) => withCalendarDays(fields ?? [], item)) : value;
9412
+ }
9413
+ return isPlainRecord(value) ? withCalendarDays(fields ?? [], value) : value;
9414
+ }
9415
+ if (field.type === "block" && Array.isArray(value)) {
9416
+ return mapItems(value, (item) => blockItemWithDays(field, item));
9417
+ }
9418
+ return value;
9419
+ };
9420
+ entryAsRead = (fields, data) => withItemKeys(fields, withCalendarDays(fields, data), "content");
9353
9421
  envFlag2 = (name) => typeof process !== "undefined" && process.env[name] === "true";
9354
9422
  LOG_LEVELS2 = {
9355
9423
  DEBUG: 0,
@@ -10317,7 +10385,6 @@ var init_generate_ai_content = __esm({
10317
10385
  };
10318
10386
  isDataOnlyFormat = (format) => format === "json" || format === "yaml";
10319
10387
  asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
10320
- init_item_keys();
10321
10388
  DEFAULT_SEO_FIELD_NAMES = {
10322
10389
  title: "metaTitle",
10323
10390
  description: "metaDescription",
@@ -11624,7 +11691,7 @@ var init_generate_ai_content = __esm({
11624
11691
  absolutePath
11625
11692
  };
11626
11693
  }
11627
- doc.data = withItemKeys(fields, doc.data, "content");
11694
+ doc.data = entryAsRead(fields, doc.data);
11628
11695
  if (options.resolveReferences !== false) {
11629
11696
  doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess, {
11630
11697
  entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
@@ -11728,7 +11795,7 @@ var init_generate_ai_content = __esm({
11728
11795
  } else if (input.format === "yaml") {
11729
11796
  content = serializeYaml(input.data ?? {}, existingRaw);
11730
11797
  } else {
11731
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
11798
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
11732
11799
  }
11733
11800
  await atomicWriteFile2(absolutePath, content);
11734
11801
  const liveIndex = this._idIndex;
@@ -12324,6 +12391,7 @@ var init_generate_ai_content = __esm({
12324
12391
  FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
12325
12392
  "string",
12326
12393
  "number",
12394
+ "date",
12327
12395
  "datetime",
12328
12396
  "boolean",
12329
12397
  "select",
@@ -14595,7 +14663,6 @@ var init_generate_ai_content = __esm({
14595
14663
  };
14596
14664
  AI_USER_ID = "canopycms-ai";
14597
14665
  init_error3();
14598
- init_item_keys();
14599
14666
  log10 = createDebugLogger2({ prefix: "ContentListing" });
14600
14667
  normalizePath = (root, target) => {
14601
14668
  const result = validateAndNormalizePath(root, target);
@@ -14705,7 +14772,7 @@ var init_generate_ai_content = __esm({
14705
14772
  fs20.stat(absolutePath),
14706
14773
  readEntryData(absolutePath, format, bodyField)
14707
14774
  ]);
14708
- const data = withItemKeys(entryType?.schema ?? [], rawData, "content");
14775
+ const data = entryAsRead(entryType?.schema ?? [], rawData);
14709
14776
  const item = {
14710
14777
  logicalPath: entryLogicalPath(collection.logicalPath, slug),
14711
14778
  contentId,
@@ -16599,7 +16666,7 @@ var scalarFieldTypes3, markdownFieldTypes3, primitiveFieldTypes3, fieldTypes3;
16599
16666
  var init_types3 = __esm({
16600
16667
  "dist/config/types.js"() {
16601
16668
  "use strict";
16602
- scalarFieldTypes3 = ["string", "number", "boolean", "datetime", "code"];
16669
+ scalarFieldTypes3 = ["string", "number", "boolean", "date", "datetime", "code"];
16603
16670
  markdownFieldTypes3 = ["markdown", "mdx"];
16604
16671
  primitiveFieldTypes3 = [...scalarFieldTypes3, ...markdownFieldTypes3];
16605
16672
  fieldTypes3 = [
@@ -68,7 +68,7 @@ var init_atomic_write = __esm({
68
68
 
69
69
  // dist/validation/field-traversal.js
70
70
  function isPlainRecord(value) {
71
- return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
72
72
  }
73
73
  function resolveBlockItem(blockField, item) {
74
74
  const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
@@ -573,7 +573,7 @@ var scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes;
573
573
  var init_types2 = __esm({
574
574
  "dist/config/types.js"() {
575
575
  "use strict";
576
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
576
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
577
577
  markdownFieldTypes = ["markdown", "mdx"];
578
578
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
579
579
  fieldTypes = [
@@ -1640,6 +1640,70 @@ function isBlockStructuralKey(key) {
1640
1640
  // dist/utils/content-serialize.js
1641
1641
  init_item_keys();
1642
1642
 
1643
+ // dist/utils/calendar-day.js
1644
+ init_item_keys();
1645
+ init_field_traversal();
1646
+ var DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
1647
+ var MS_PER_DAY = 864e5;
1648
+ var isRealDay = (year, month, day) => {
1649
+ const lastOfMonth = /* @__PURE__ */ new Date(0);
1650
+ lastOfMonth.setUTCFullYear(year, month, 0);
1651
+ return month >= 1 && month <= 12 && day >= 1 && day <= lastOfMonth.getUTCDate();
1652
+ };
1653
+ var dayParts = (value) => {
1654
+ const match = typeof value === "string" ? DAY.exec(value) : null;
1655
+ if (!match)
1656
+ return void 0;
1657
+ const [year, month, day] = match.slice(1).map(Number);
1658
+ return isRealDay(year, month, day) ? [year, month, day] : void 0;
1659
+ };
1660
+ var isCalendarDay = (value) => dayParts(value) !== void 0;
1661
+ var dayOfBareDate = (value) => value.getTime() % MS_PER_DAY === 0 ? value.toISOString().slice(0, 10) : void 0;
1662
+ var mapItems = (items, mapItem) => {
1663
+ const next = items.map((item) => isPlainRecord(item) ? mapItem(item) : item);
1664
+ return next.every((item, i) => item === items[i]) ? items : next;
1665
+ };
1666
+ var blockItemWithDays = (field, item) => {
1667
+ const resolved = resolveBlockItem(field, item);
1668
+ if (!resolved)
1669
+ return item;
1670
+ const data = withCalendarDays(resolved.fields, resolved.data);
1671
+ if (data === resolved.data)
1672
+ return item;
1673
+ return resolved.data === item ? data : { ...item, value: data };
1674
+ };
1675
+ var valueWithDays = (field, value) => {
1676
+ if (field.type === "date") {
1677
+ return value instanceof Date ? dayOfBareDate(value) ?? value : value;
1678
+ }
1679
+ if (field.type === "object") {
1680
+ const { fields, list } = field;
1681
+ if (list) {
1682
+ return Array.isArray(value) ? mapItems(value, (item) => withCalendarDays(fields ?? [], item)) : value;
1683
+ }
1684
+ return isPlainRecord(value) ? withCalendarDays(fields ?? [], value) : value;
1685
+ }
1686
+ if (field.type === "block" && Array.isArray(value)) {
1687
+ return mapItems(value, (item) => blockItemWithDays(field, item));
1688
+ }
1689
+ return value;
1690
+ };
1691
+ function withCalendarDays(fields, data) {
1692
+ let out = data;
1693
+ for (const field of fields) {
1694
+ if (field.type === "group") {
1695
+ out = withCalendarDays(field.fields, out);
1696
+ continue;
1697
+ }
1698
+ const value = out[field.name];
1699
+ const next = valueWithDays(field, value);
1700
+ if (next !== value)
1701
+ out = { ...out, [field.name]: next };
1702
+ }
1703
+ return out;
1704
+ }
1705
+ var entryAsRead = (fields, data) => withItemKeys(fields, withCalendarDays(fields, data), "content");
1706
+
1643
1707
  // dist/utils/debug.js
1644
1708
  var envFlag = (name) => typeof process !== "undefined" && process.env[name] === "true";
1645
1709
  var LOG_LEVELS = {
@@ -3235,9 +3299,9 @@ function asJson(value) {
3235
3299
  const key = identityKey(value);
3236
3300
  return key === void 0 || key === "undefined" ? void 0 : JSON.parse(key);
3237
3301
  }
3238
- function frontmatterAsRead(raw) {
3302
+ function frontmatterAsRead(raw, fields) {
3239
3303
  try {
3240
- return asJson(matter(raw, {}).data);
3304
+ return asJson(withCalendarDays(fields, matter(raw, {}).data));
3241
3305
  } catch {
3242
3306
  return void 0;
3243
3307
  }
@@ -3305,7 +3369,7 @@ ${asKey ? `${text}: v` : `v: ${text}`}
3305
3369
  return true;
3306
3370
  }
3307
3371
  }
3308
- function serializeFrontmatter(editorBody, data, existingRaw, format) {
3372
+ function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []) {
3309
3373
  const file = { content: bodyToWrite(editorBody, data, format, existingRaw) };
3310
3374
  if (existingRaw === void 0)
3311
3375
  return matter.stringify(file, data);
@@ -3313,7 +3377,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
3313
3377
  if (existingFrontmatter === void 0)
3314
3378
  return matter.stringify(file, data);
3315
3379
  const onDisk = parseDocument2(existingFrontmatter);
3316
- const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw), onDisk.toJS()) : data;
3380
+ const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw, fields), onDisk.toJS()) : data;
3317
3381
  const reconciled = reconcileYamlSource(existingFrontmatter, isPlainRecord2(toReconcile) ? toReconcile : data, { rootAtColumnZero: true, quote: readsAsAnotherValue });
3318
3382
  if (reconciled === void 0)
3319
3383
  return matter.stringify(file, data);
@@ -3325,7 +3389,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
3325
3389
  }
3326
3390
  }
3327
3391
  });
3328
- if (isDeepStrictEqual2(frontmatterAsRead(written), asJson(data)))
3392
+ if (isDeepStrictEqual2(frontmatterAsRead(written, fields), asJson(data)))
3329
3393
  return written;
3330
3394
  log2.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
3331
3395
  return matter.stringify(file, data);
@@ -3639,9 +3703,6 @@ function linkOnlyReferenceData(data, format, fields) {
3639
3703
  return rest;
3640
3704
  }
3641
3705
 
3642
- // dist/content-store.js
3643
- init_item_keys();
3644
-
3645
3706
  // dist/static/seo.js
3646
3707
  var DEFAULT_SEO_FIELD_NAMES = {
3647
3708
  title: "metaTitle",
@@ -5834,7 +5895,7 @@ var ContentStore = class {
5834
5895
  absolutePath
5835
5896
  };
5836
5897
  }
5837
- doc.data = withItemKeys(fields, doc.data, "content");
5898
+ doc.data = entryAsRead(fields, doc.data);
5838
5899
  if (options.resolveReferences !== false) {
5839
5900
  doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess, {
5840
5901
  entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
@@ -5938,7 +5999,7 @@ var ContentStore = class {
5938
5999
  } else if (input.format === "yaml") {
5939
6000
  content = serializeYaml(input.data ?? {}, existingRaw);
5940
6001
  } else {
5941
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
6002
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
5942
6003
  }
5943
6004
  await atomicWriteFile(absolutePath, content);
5944
6005
  const liveIndex = this._idIndex;
@@ -6830,6 +6891,7 @@ _${field.description}_` : "";
6830
6891
  switch (field.type) {
6831
6892
  case "string":
6832
6893
  case "number":
6894
+ case "date":
6833
6895
  case "datetime":
6834
6896
  return `${heading} ${label}${descriptionLine}
6835
6897
 
@@ -6908,6 +6970,7 @@ ${items.map((item) => `- ${item}`).join("\n")}`;
6908
6970
  var FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
6909
6971
  "string",
6910
6972
  "number",
6973
+ "date",
6911
6974
  "datetime",
6912
6975
  "boolean",
6913
6976
  "select",
@@ -10970,7 +11033,6 @@ import path24 from "node:path";
10970
11033
  import matter3 from "gray-matter";
10971
11034
  import { parse as yamlParse2 } from "yaml";
10972
11035
  init_error();
10973
- init_item_keys();
10974
11036
  var log10 = createDebugLogger({ prefix: "ContentListing" });
10975
11037
  var normalizePath = (root, target) => {
10976
11038
  const result = validateAndNormalizePath(root, target);
@@ -11141,7 +11203,7 @@ var listCollectionEntries = async (root, collection, onSkip, readsId) => {
11141
11203
  fs20.stat(absolutePath),
11142
11204
  readEntryData(absolutePath, format, bodyField)
11143
11205
  ]);
11144
- const data = withItemKeys(entryType?.schema ?? [], rawData, "content");
11206
+ const data = entryAsRead(entryType?.schema ?? [], rawData);
11145
11207
  const item = {
11146
11208
  logicalPath: entryLogicalPath(collection.logicalPath, slug),
11147
11209
  contentId,
@@ -11254,6 +11316,11 @@ function validateScalar(field, value, path27) {
11254
11316
  return [{ fieldPath: path27, message: "Expected true or false" }];
11255
11317
  }
11256
11318
  return [];
11319
+ case "date":
11320
+ if (value !== "" && !isCalendarDay(value)) {
11321
+ return [{ fieldPath: path27, message: "Expected a real date, written YYYY-MM-DD" }];
11322
+ }
11323
+ return [];
11257
11324
  case "datetime":
11258
11325
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
11259
11326
  return [{ fieldPath: path27, message: "Expected a valid date/time string" }];
@@ -11408,7 +11475,7 @@ function findInvalidEntries(items) {
11408
11475
  for (const item of items) {
11409
11476
  if (!item.schema)
11410
11477
  continue;
11411
- const data = normalizeDatesDeep(item.data);
11478
+ const data = normalizeDatesDeep(withCalendarDays(item.schema, item.data));
11412
11479
  const errors = validateEntryData(item.schema, data);
11413
11480
  if (errors.length > 0) {
11414
11481
  invalid.push({ entryPath: item.entryPath, errors });
package/dist/cli/init.js CHANGED
@@ -26,7 +26,7 @@ var scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes;
26
26
  var init_types = __esm({
27
27
  "dist/config/types.js"() {
28
28
  "use strict";
29
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
29
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
30
30
  markdownFieldTypes = ["markdown", "mdx"];
31
31
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
32
32
  fieldTypes = [
@@ -10,10 +10,10 @@ import type { PermissionPath } from '../authorization/types.js';
10
10
  import type { EntryLinkUrlResolver } from '../entry-link-resolver.js';
11
11
  import type { CropRect } from '../assets/transform-directives.js';
12
12
  import type { SeoFieldLocation } from '../static/seo.js';
13
- declare const scalarFieldTypes: readonly ["string", "number", "boolean", "datetime", "code"];
13
+ declare const scalarFieldTypes: readonly ["string", "number", "boolean", "date", "datetime", "code"];
14
14
  export declare const markdownFieldTypes: readonly ["markdown", "mdx"];
15
- export declare const primitiveFieldTypes: readonly ["string", "number", "boolean", "datetime", "code", "markdown", "mdx"];
16
- export declare const fieldTypes: readonly ["string", "number", "boolean", "datetime", "code", "markdown", "mdx", "image", "select", "reference", "object", "block", "group"];
15
+ export declare const primitiveFieldTypes: readonly ["string", "number", "boolean", "date", "datetime", "code", "markdown", "mdx"];
16
+ export declare const fieldTypes: readonly ["string", "number", "boolean", "date", "datetime", "code", "markdown", "mdx", "image", "select", "reference", "object", "block", "group"];
17
17
  export type PrimitiveFieldType = (typeof primitiveFieldTypes)[number];
18
18
  export type FieldType = (typeof fieldTypes)[number];
19
19
  export type ContentFormat = 'md' | 'mdx' | 'json' | 'yaml';
@@ -211,7 +211,7 @@ export interface ObjectFieldConfig extends BaseFieldConfig {
211
211
  type: 'object';
212
212
  fields: FieldConfig[];
213
213
  /**
214
- * On a `list: true` object, a direct `string` or `number` child whose value titles each
214
+ * On a `list: true` object, a direct `string`, `number` or `date` child whose value titles each
215
215
  * item's card; a blank or non-finite value falls back to the item's first non-blank `string`
216
216
  * field, then "Item N".
217
217
  */
@@ -2,7 +2,7 @@
2
2
  * Type definitions for CanopyCMS configuration.
3
3
  * These are pure TypeScript types - Zod schemas are in ./schemas/
4
4
  */
5
- const scalarFieldTypes = ['string', 'number', 'boolean', 'datetime', 'code'];
5
+ const scalarFieldTypes = ['string', 'number', 'boolean', 'date', 'datetime', 'code'];
6
6
  export const markdownFieldTypes = ['markdown', 'mdx'];
7
7
  export const primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
8
8
  // 'image' is not a primitive: it has a structured value (ImageFieldValue) and
@@ -34,9 +34,9 @@ export declare const ensureReferenceFieldsHaveScope: (fields: unknown) => void;
34
34
  export declare const forEachReferenceField: (fields: unknown, visit: (field: Record<string, unknown>) => void) => void;
35
35
  /**
36
36
  * Check that every `itemLabel` is non-blank text and, like every `itemTitleField`, sits on a
37
- * `list: true` object, and that every `itemTitleField` names a direct child of type `string` or
38
- * `number`, the only values the card heading shows as typed (a reference would show its raw id, a
39
- * select its stored value). Throws an error when it does not. Entry schemas reach the editor
37
+ * `list: true` object, and that every `itemTitleField` names a direct child of type `string`,
38
+ * `number` or `date`, the only values the card heading shows as typed (a reference would show its
39
+ * raw id, a select its stored value). Throws an error when it does not. Entry schemas reach the editor
40
40
  * through these checks, not the zod field schema.
41
41
  */
42
42
  export declare const ensureItemTitleFieldsExist: (fields: unknown) => void;
@@ -68,6 +68,12 @@ export declare const ensureStampOnPublishPlacement: (fields: unknown, scopeLabel
68
68
  * `ensureStampOnPublishPlacement`. Throws on the first problem.
69
69
  */
70
70
  export declare const ensureMaxItemsPlacement: (fields: unknown, scopeLabel?: string) => void;
71
+ /**
72
+ * Check that no `date` field is `list: true`: the editor has one picker for one day, and an object
73
+ * list with a `date` child gives a list of days its rows. Recurses into groups, objects and block
74
+ * templates like `ensureStampOnPublishPlacement`. Throws on the first problem.
75
+ */
76
+ export declare const ensureSingleDateFields: (fields: unknown, scopeLabel?: string) => void;
71
77
  /**
72
78
  * Check every markdown and mdx field's `mdxAllow` against its schema and its `renderAs`,
73
79
  * `mdxAllow` and `executable` against each other. Throws on the first problem.
@@ -110,9 +110,9 @@ export const forEachReferenceField = (fields, visit) => {
110
110
  };
111
111
  /**
112
112
  * Check that every `itemLabel` is non-blank text and, like every `itemTitleField`, sits on a
113
- * `list: true` object, and that every `itemTitleField` names a direct child of type `string` or
114
- * `number`, the only values the card heading shows as typed (a reference would show its raw id, a
115
- * select its stored value). Throws an error when it does not. Entry schemas reach the editor
113
+ * `list: true` object, and that every `itemTitleField` names a direct child of type `string`,
114
+ * `number` or `date`, the only values the card heading shows as typed (a reference would show its
115
+ * raw id, a select its stored value). Throws an error when it does not. Entry schemas reach the editor
116
116
  * through these checks, not the zod field schema.
117
117
  */
118
118
  export const ensureItemTitleFieldsExist = (fields) => {
@@ -140,8 +140,8 @@ export const ensureItemTitleFieldsExist = (fields) => {
140
140
  if (typeof f.itemTitleField !== 'string' || !child) {
141
141
  throw new Error(`${label}, which is not one of its child fields`);
142
142
  }
143
- if (child.type !== 'string' && child.type !== 'number') {
144
- throw new Error(`${label}, which must name a string or number field`);
143
+ if (child.type !== 'string' && child.type !== 'number' && child.type !== 'date') {
144
+ throw new Error(`${label}, which must name a string, number or date field`);
145
145
  }
146
146
  }
147
147
  ensureItemTitleFieldsExist(children);
@@ -327,6 +327,31 @@ export const ensureMaxItemsPlacement = (fields, scopeLabel = 'entry schema') =>
327
327
  };
328
328
  check(fields);
329
329
  };
330
+ /**
331
+ * Check that no `date` field is `list: true`: the editor has one picker for one day, and an object
332
+ * list with a `date` child gives a list of days its rows. Recurses into groups, objects and block
333
+ * templates like `ensureStampOnPublishPlacement`. Throws on the first problem.
334
+ */
335
+ export const ensureSingleDateFields = (fields, scopeLabel = 'entry schema') => {
336
+ const check = (scopeFields) => {
337
+ if (!Array.isArray(scopeFields))
338
+ return;
339
+ for (const field of scopeFields) {
340
+ const f = field;
341
+ if (f?.type === 'date' && f.list === true) {
342
+ throw new Error(`${scopeLabel}: field "${String(f.name)}" is a date with list: true; use an object list with a date field inside`);
343
+ }
344
+ if (f?.type === 'group' || f?.type === 'object')
345
+ check(f.fields);
346
+ if (f?.type === 'block' && Array.isArray(f.templates)) {
347
+ for (const template of f.templates) {
348
+ check(template.fields);
349
+ }
350
+ }
351
+ }
352
+ };
353
+ check(fields);
354
+ };
330
355
  /**
331
356
  * Check every markdown and mdx field's `mdxAllow` against its schema and its `renderAs`,
332
357
  * `mdxAllow` and `executable` against each other. Throws on the first problem.
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import matter from 'gray-matter';
7
7
  import { parse as yamlParse } from 'yaml';
8
8
  import { findBodyFieldName } from './utils/body-field.js';
9
+ import { entryAsRead } from './utils/calendar-day.js';
9
10
  import { computeEntryUrl } from './utils/entry-url.js';
10
11
  import { asRecord, getFormatExtension } from './utils/format.js';
11
12
  import { resolveCollectionPath, toLogicalCollectionPath } from './content-id-index.js';
@@ -15,7 +16,6 @@ import { isNotFoundError, getErrorMessage } from './utils/error.js';
15
16
  import { createDebugLogger } from './utils/debug.js';
16
17
  import { ContentStore, ContentStoreError, createReferenceResolveCache, } from './content-store.js';
17
18
  import { isBuildMode } from './build-mode.js';
18
- import { withItemKeys } from './item-keys.js';
19
19
  const log = createDebugLogger({ prefix: 'ContentListing' });
20
20
  /** Validate and normalize `target` under `root`; throws ContentStoreError on a traversal. */
21
21
  const normalizePath = (root, target) => {
@@ -444,8 +444,7 @@ export const listCollectionEntries = async (root, collection, onSkip, readsId) =
444
444
  fs.stat(absolutePath),
445
445
  readEntryData(absolutePath, format, bodyField),
446
446
  ]);
447
- // Keyed as `ContentStore.read` keys it, so every reader names an item alike.
448
- const data = withItemKeys(entryType?.schema ?? [], rawData, 'content');
447
+ const data = entryAsRead(entryType?.schema ?? [], rawData);
449
448
  const item = {
450
449
  logicalPath: entryLogicalPath(collection.logicalPath, slug),
451
450
  contentId,
@@ -21,7 +21,7 @@ import { serializeJson } from './utils/json-source-splice.js';
21
21
  import { withLock } from './utils/async-mutex.js';
22
22
  import { assertContentWriteLockHeld, ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
23
23
  import { findBodyFieldName, linkOnlyReferenceData } from './utils/body-field.js';
24
- import { withItemKeys } from './item-keys.js';
24
+ import { entryAsRead } from './utils/calendar-day.js';
25
25
  import { buildMissingReference, buildResolvedReference, buildRestrictedReference, } from './entry-schema.js';
26
26
  import { warnDanglingReference } from './dangling-reference-log.js';
27
27
  import { canopyLogError } from './utils/logger.js';
@@ -786,9 +786,9 @@ export class ContentStore {
786
786
  absolutePath,
787
787
  };
788
788
  }
789
- // Keys an item saved without one by its content, before references resolve into it, as the
790
- // listing and the write path do (`item-keys.ts`).
791
- doc.data = withItemKeys(fields, doc.data, 'content');
789
+ // Before references resolve into it. An item saved without a key is keyed by its content, as
790
+ // the listing and the write path do (`item-keys.ts`).
791
+ doc.data = entryAsRead(fields, doc.data);
792
792
  if (options.resolveReferences !== false) {
793
793
  doc.data = await this.resolveReferencesInData(doc.data, fields, undefined, options.referenceAccess, {
794
794
  entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
@@ -1031,7 +1031,7 @@ export class ContentStore {
1031
1031
  content = serializeYaml(input.data ?? {}, existingRaw);
1032
1032
  }
1033
1033
  else {
1034
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
1034
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
1035
1035
  }
1036
1036
  await atomicWriteFile(absolutePath, content);
1037
1037
  // Update the ID index after a successful write. Look up and mutate the LIVE index