camox 0.46.0 → 0.48.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 (82) hide show
  1. package/dist/core/createApp.d.ts +19 -6
  2. package/dist/core/createApp.js +7 -2
  3. package/dist/core/createBlock.js +54 -43
  4. package/dist/core/createCollection.d.ts +25 -7
  5. package/dist/core/createCollection.js +28 -9
  6. package/dist/core/createLayout.d.ts +98 -71
  7. package/dist/core/createLayout.js +10 -16
  8. package/dist/core/derivedRoutes.js +1 -1
  9. package/dist/core/editing/BlockEditingRuntime.d.ts +3 -2
  10. package/dist/core/editing/CompleteBlockEditingRuntime.js +2 -1
  11. package/dist/core/editing/createEditableBlock.d.ts +59 -19
  12. package/dist/core/editing/createEditableBlock.js +162 -151
  13. package/dist/core/editing/useLatestRecord.js +7 -7
  14. package/dist/core/layoutContext.d.ts +54 -0
  15. package/dist/core/layoutContext.js +66 -0
  16. package/dist/core/lib/contentType.d.ts +91 -4
  17. package/dist/core/lib/contentType.js +84 -7
  18. package/dist/core/lib/recordScope.js +19 -17
  19. package/dist/core/lib/reference.d.ts +6 -2
  20. package/dist/core/recordItem.js +55 -2
  21. package/dist/core/requestContext.d.ts +18 -0
  22. package/dist/core/structuredData.d.ts +126 -0
  23. package/dist/core/structuredData.js +40 -0
  24. package/dist/features/canvas/canvasCamera.js +21 -6
  25. package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
  26. package/dist/features/canvas/useCanvasCamera.js +16 -11
  27. package/dist/features/content/ContentCollection.js +13 -7
  28. package/dist/features/content/collection-form.js +6 -2
  29. package/dist/features/content/components/MissingFromLocale.js +1 -1
  30. package/dist/features/og/pageImage.js +89 -0
  31. package/dist/features/page/CuratedPageContent.js +45 -32
  32. package/dist/features/page/DerivedPageContent.js +49 -22
  33. package/dist/features/page/SharedLayoutContent.js +26 -22
  34. package/dist/features/preview/CamoxPreview.js +176 -145
  35. package/dist/features/preview/components/AddBlockSheet.js +1 -0
  36. package/dist/features/preview/components/BlockActionsPopover.js +8 -6
  37. package/dist/features/preview/components/PageInfo.js +244 -264
  38. package/dist/features/preview/components/PageTree.js +63 -53
  39. package/dist/features/preview/components/PreviewToolbar.js +74 -85
  40. package/dist/features/preview/components/RecordCard.js +50 -34
  41. package/dist/features/preview/components/RecordCombobox.js +12 -5
  42. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
  43. package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
  44. package/dist/features/preview/components/RepeatableItemsList.js +62 -46
  45. package/dist/features/preview/components/SelectionEditor.js +3 -2
  46. package/dist/features/preview/components/useFieldWriter.js +2 -4
  47. package/dist/features/preview/components/useRecordView.js +2 -2
  48. package/dist/features/preview/entityName.js +11 -0
  49. package/dist/features/preview/pageRecord.js +17 -18
  50. package/dist/features/provider/CamoxProvider.js +8 -16
  51. package/dist/features/provider/CoreCamoxProvider.js +1 -6
  52. package/dist/features/provider/signInShortcut.js +60 -0
  53. package/dist/features/routes/pageRuntime.d.ts +15 -1
  54. package/dist/features/routes/pageRuntime.js +35 -38
  55. package/dist/features/runtime/RuntimeChrome.js +2 -2
  56. package/dist/features/runtime/breadcrumbs.js +68 -0
  57. package/dist/features/runtime/livePages.js +3 -2
  58. package/dist/features/runtime/pageApp.js +11 -39
  59. package/dist/features/runtime/pageGraph.js +92 -0
  60. package/dist/features/runtime/projectLogo.js +110 -0
  61. package/dist/features/runtime/runtime.d.ts +11 -6
  62. package/dist/features/runtime/runtime.js +278 -55
  63. package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
  64. package/dist/features/runtime/runtimeRoutes.js +1 -0
  65. package/dist/features/runtime/structuredData.js +213 -0
  66. package/dist/features/studio/components/ChatSidebar.js +5 -5
  67. package/dist/features/studio/components/ProjectMenu.js +127 -100
  68. package/dist/features/studio/components/SocialProfilesDialog.js +342 -0
  69. package/dist/features/studio/components/ThreadMessages.js +2 -2
  70. package/dist/features/vite/blockBoilerplate.js +3 -2
  71. package/dist/features/vite/layoutGeneration.js +3 -2
  72. package/dist/features/vite/vite.js +1 -0
  73. package/dist/lib/queries.d.ts +4 -0
  74. package/dist/lib/queries.js +1 -0
  75. package/dist/lib/studio-reads.js +3 -1
  76. package/dist/package.js +1 -1
  77. package/dist/studio.css +1 -1
  78. package/package.json +5 -4
  79. package/skills/camox/references/block-definitions.md +105 -25
  80. package/skills/camox/references/layout-definitions.md +53 -17
  81. package/skills/camox/references/routed-layouts.md +17 -14
  82. package/dist/features/provider/LocalhostPreviewProvider.js +0 -158
@@ -3,7 +3,7 @@ import { CamoxDocument } from "./defineDocument.js";
3
3
  import { ArtifactRenderProps } from "./lib/artifact.js";
4
4
  import { InlineTextStyles } from "./lib/textStyles.js";
5
5
  import { ReferenceContent, ReferenceScope } from "./lib/reference.js";
6
- import { Layout, RecordPages } from "./createLayout.js";
6
+ import { Layout } from "./createLayout.js";
7
7
  import { Collection } from "./createCollection.js";
8
8
  import { IconValue } from "./lib/iconTypes.js";
9
9
  import { ArtifactSource, EmbedURL, FileValue, ImageValue, LinkValue } from "./lib/contentType.js";
@@ -416,17 +416,19 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
416
416
  Component: ({ blockData, mode, isFirstBlock, showAddBlockTop, showAddBlockBottom, addBlockAfterPosition }: BlockComponentProps<{
417
417
  [x: string]: unknown;
418
418
  }>) => import("react").JSX.Element;
419
- LayoutItem: ({ collectionId, labelField, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
419
+ LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
420
420
  id: string;
421
421
  title: string;
422
422
  description: string;
423
423
  contentSchema: {
424
+ toStructuredData?: unknown;
424
425
  type: "object";
425
426
  title: string;
426
427
  description: string;
427
428
  properties: Record<string, import("@sinclair/typebox").TSchema>;
428
429
  required: string[];
429
430
  toMarkdown: string[];
431
+ toTitle: string;
430
432
  };
431
433
  settingsSchema: {
432
434
  type: "object";
@@ -458,6 +460,7 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
458
460
  settings: Record<string, unknown>;
459
461
  slot: null;
460
462
  summary: string;
463
+ title: string;
461
464
  position: string;
462
465
  createdAt: number;
463
466
  updatedAt: number;
@@ -855,17 +858,19 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
855
858
  Component: ({ blockData, mode, isFirstBlock, showAddBlockTop, showAddBlockBottom, addBlockAfterPosition }: BlockComponentProps<{
856
859
  [x: string]: unknown;
857
860
  }>) => import("react").JSX.Element;
858
- LayoutItem: ({ collectionId, labelField, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
861
+ LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
859
862
  id: string;
860
863
  title: string;
861
864
  description: string;
862
865
  contentSchema: {
866
+ toStructuredData?: unknown;
863
867
  type: "object";
864
868
  title: string;
865
869
  description: string;
866
870
  properties: Record<string, import("@sinclair/typebox").TSchema>;
867
871
  required: string[];
868
872
  toMarkdown: string[];
873
+ toTitle: string;
869
874
  };
870
875
  settingsSchema: {
871
876
  type: "object";
@@ -897,6 +902,7 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
897
902
  settings: Record<string, unknown>;
898
903
  slot: null;
899
904
  summary: string;
905
+ title: string;
900
906
  position: string;
901
907
  createdAt: number;
902
908
  updatedAt: number;
@@ -925,12 +931,14 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
925
931
  title: string;
926
932
  description: string;
927
933
  contentSchema: {
934
+ toStructuredData?: unknown;
928
935
  type: "object";
929
936
  title: string;
930
937
  description: string;
931
938
  properties: Record<string, import("@sinclair/typebox").TSchema>;
932
939
  required: string[];
933
940
  toMarkdown: string[];
941
+ toTitle: string;
934
942
  };
935
943
  settingsSchema: {
936
944
  type: "object";
@@ -947,7 +955,10 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
947
955
  synced: boolean;
948
956
  }[];
949
957
  layouts: {
950
- recordPages?: RecordPages | undefined;
958
+ recordPages?: {
959
+ collectionId: string;
960
+ field: string;
961
+ } | undefined;
951
962
  layoutId: string;
952
963
  kind: "curated" | "derived" | "singleton";
953
964
  description: string;
@@ -978,11 +989,13 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
978
989
  })[];
979
990
  }[];
980
991
  collections: {
992
+ contentSchema: any;
993
+ toStructuredData?: Record<string, unknown> | undefined;
981
994
  collectionId: string;
982
995
  title: string;
983
996
  description: string;
984
- label: string;
985
- contentSchema: any;
997
+ toTitle: string;
998
+ toDescription: string | null;
986
999
  }[];
987
1000
  homepage: {
988
1001
  layoutId: string;
@@ -134,7 +134,10 @@ function createApp({ blocks, lazyBlocks = {}, layouts = [], collections = [], do
134
134
  kind: layout._internal.kind,
135
135
  description: layout._internal.description,
136
136
  blocks: layout._internal.blockDefinitions,
137
- ...layout._internal.recordPages && { recordPages: layout._internal.recordPages }
137
+ ...layout._internal.recordPages && { recordPages: {
138
+ collectionId: layout._internal.recordPages.collectionId,
139
+ field: layout._internal.recordPages.field
140
+ } }
138
141
  }));
139
142
  const typesUsedInLayouts = new Set(layoutDefinitions.flatMap((layout) => layout.blocks.map((block) => block.type)));
140
143
  return {
@@ -154,7 +157,9 @@ function createApp({ blocks, lazyBlocks = {}, layouts = [], collections = [], do
154
157
  collectionId: _internal.id,
155
158
  title: _internal.title,
156
159
  description: _internal.description,
157
- label: _internal.label,
160
+ toTitle: _internal.toTitle,
161
+ toDescription: _internal.toDescription,
162
+ ..._internal.toStructuredData && { toStructuredData: _internal.toStructuredData },
158
163
  contentSchema: JSON.parse(JSON.stringify(_internal.contentSchema))
159
164
  })),
160
165
  homepage: await getHomepage()
@@ -1,6 +1,6 @@
1
1
  import { artifactFrameProps, artifactStandInProps, isArtifactSource } from "./lib/artifact.js";
2
2
  import { artifactData } from "./lib/artifactData.js";
3
- import { resolveBlockFields, resolveToMarkdown } from "./lib/contentType.js";
3
+ import { resolveBlockFields, resolveToMarkdown, resolveToStructuredData, resolveToTitle } from "./lib/contentType.js";
4
4
  import { useProjectUuid } from "../lib/auth.js";
5
5
  import { useLocation, useNavigate } from "../features/navigation/navigation.js";
6
6
  import { useOptionalCamoxApp } from "../features/provider/components/CamoxAppContext.js";
@@ -96,6 +96,7 @@ function buildPeekItems(properties, blockId, parentItemId, content, items, count
96
96
  content: itemContent,
97
97
  settings: property.defaultItemSettings ? { ...property.defaultItemSettings } : null,
98
98
  summary: "",
99
+ title: "",
99
100
  position,
100
101
  createdAt: 0,
101
102
  updatedAt: 0,
@@ -434,7 +435,7 @@ function createViewBlock(options) {
434
435
  }, record.id));
435
436
  };
436
437
  const RecordScope = (t0) => {
437
- const $ = c(13);
438
+ const $ = c(14);
438
439
  const { block, schema, record, children } = t0;
439
440
  const hop = block.recordHop ? 2 : 1;
440
441
  let t1;
@@ -452,10 +453,11 @@ function createViewBlock(options) {
452
453
  $[4] = t1;
453
454
  } else t1 = $[4];
454
455
  let t2;
455
- if ($[5] !== children || $[6] !== hop || $[7] !== record.id || $[8] !== record.label) {
456
+ if ($[5] !== children || $[6] !== hop || $[7] !== record.description || $[8] !== record.id || $[9] !== record.title) {
456
457
  t2 = children({
457
458
  id: record.id,
458
- label: record.label,
459
+ title: record.title,
460
+ description: record.description,
459
461
  Field,
460
462
  Image: ReferenceImage,
461
463
  File: ReferenceFile,
@@ -469,12 +471,13 @@ function createViewBlock(options) {
469
471
  });
470
472
  $[5] = children;
471
473
  $[6] = hop;
472
- $[7] = record.id;
473
- $[8] = record.label;
474
- $[9] = t2;
475
- } else t2 = $[9];
474
+ $[7] = record.description;
475
+ $[8] = record.id;
476
+ $[9] = record.title;
477
+ $[10] = t2;
478
+ } else t2 = $[10];
476
479
  let t3;
477
- if ($[10] !== t1 || $[11] !== t2) {
480
+ if ($[11] !== t1 || $[12] !== t2) {
478
481
  t3 = /* @__PURE__ */ jsx(RepeaterContext.Provider, {
479
482
  value: null,
480
483
  children: /* @__PURE__ */ jsx(Context.Provider, {
@@ -482,10 +485,10 @@ function createViewBlock(options) {
482
485
  children: t2
483
486
  })
484
487
  });
485
- $[10] = t1;
486
- $[11] = t2;
487
- $[12] = t3;
488
- } else t3 = $[12];
488
+ $[11] = t1;
489
+ $[12] = t2;
490
+ $[13] = t3;
491
+ } else t3 = $[13];
489
492
  return t3;
490
493
  };
491
494
  const AssetList = ({ name, children, primitive }) => {
@@ -634,7 +637,7 @@ function createViewBlock(options) {
634
637
  return t1;
635
638
  };
636
639
  const Component = (props) => {
637
- const $ = c(22);
640
+ const $ = c(23);
638
641
  const editingRuntime = useBlockEditingRuntime();
639
642
  if (editingRuntime) {
640
643
  let t0;
@@ -663,66 +666,71 @@ function createViewBlock(options) {
663
666
  mergedContent = $[4];
664
667
  normalizedContent = $[5];
665
668
  }
666
- const AuthoredComponent = options.component;
669
+ const ToReact = options.toReact;
667
670
  let t0;
668
- if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
671
+ if ($[6] !== blockData.settings) {
669
672
  t0 = {
670
- position: "relative",
671
- scrollMargin: "5rem"
673
+ ...settingsDefaults,
674
+ ...blockData.settings
672
675
  };
673
- $[6] = t0;
674
- } else t0 = $[6];
676
+ $[6] = blockData.settings;
677
+ $[7] = t0;
678
+ } else t0 = $[7];
679
+ const settings = t0;
675
680
  let t1;
676
- if ($[7] !== blockData.settings) {
681
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
677
682
  t1 = {
678
- ...settingsDefaults,
679
- ...blockData.settings
683
+ position: "relative",
684
+ scrollMargin: "5rem"
680
685
  };
681
- $[7] = blockData.settings;
682
686
  $[8] = t1;
683
687
  } else t1 = $[8];
684
688
  let t2;
685
- if ($[9] !== blockData._id || $[10] !== mergedContent || $[11] !== mode || $[12] !== t1) {
689
+ if ($[9] !== blockData._id || $[10] !== mergedContent || $[11] !== mode || $[12] !== settings) {
686
690
  t2 = {
687
691
  blockId: blockData._id,
688
692
  content: mergedContent,
689
- settings: t1,
693
+ settings,
690
694
  mode
691
695
  };
692
696
  $[9] = blockData._id;
693
697
  $[10] = mergedContent;
694
698
  $[11] = mode;
695
- $[12] = t1;
699
+ $[12] = settings;
696
700
  $[13] = t2;
697
701
  } else t2 = $[13];
698
702
  let t3;
699
- if ($[14] !== normalizedContent) {
700
- t3 = /* @__PURE__ */ jsx(AuthoredComponent, { content: normalizedContent });
703
+ if ($[14] !== normalizedContent || $[15] !== settings) {
704
+ t3 = /* @__PURE__ */ jsx(ToReact, {
705
+ content: normalizedContent,
706
+ settings
707
+ });
701
708
  $[14] = normalizedContent;
702
- $[15] = t3;
703
- } else t3 = $[15];
709
+ $[15] = settings;
710
+ $[16] = t3;
711
+ } else t3 = $[16];
704
712
  let t4;
705
- if ($[16] !== t2 || $[17] !== t3) {
713
+ if ($[17] !== t2 || $[18] !== t3) {
706
714
  t4 = /* @__PURE__ */ jsx(Context.Provider, {
707
715
  value: t2,
708
716
  children: t3
709
717
  });
710
- $[16] = t2;
711
- $[17] = t3;
712
- $[18] = t4;
713
- } else t4 = $[18];
718
+ $[17] = t2;
719
+ $[18] = t3;
720
+ $[19] = t4;
721
+ } else t4 = $[19];
714
722
  let t5;
715
- if ($[19] !== blockData._id || $[20] !== t4) {
723
+ if ($[20] !== blockData._id || $[21] !== t4) {
716
724
  t5 = /* @__PURE__ */ jsx("div", {
717
725
  className: "group visual-editing-block",
718
726
  "data-camox-viewport-block": blockData._id,
719
- style: t0,
727
+ style: t1,
720
728
  children: t4
721
729
  });
722
- $[19] = blockData._id;
723
- $[20] = t4;
724
- $[21] = t5;
725
- } else t5 = $[21];
730
+ $[20] = blockData._id;
731
+ $[21] = t4;
732
+ $[22] = t5;
733
+ } else t5 = $[22];
726
734
  return t5;
727
735
  };
728
736
  const contentSchema = {
@@ -731,7 +739,9 @@ function createViewBlock(options) {
731
739
  description: options.description,
732
740
  properties: typeboxSchema.properties,
733
741
  required: Object.keys(fields.content),
734
- toMarkdown: resolveToMarkdown(options.toMarkdown, fields.settings, "block", fields.content)
742
+ toMarkdown: resolveToMarkdown(options.toMarkdown, fields.settings, "block", fields.content),
743
+ toTitle: resolveToTitle(options.toTitle, fields.settings, "block", fields.content),
744
+ ...options.toStructuredData && { toStructuredData: resolveToStructuredData(options.toStructuredData, fields.settings, "block", fields.content) }
735
745
  };
736
746
  const settingsSchema = settingsTypeboxSchema ? {
737
747
  type: "object",
@@ -810,6 +820,7 @@ function createViewBlock(options) {
810
820
  settings: settingsDefaults,
811
821
  slot: null,
812
822
  summary: "",
823
+ title: "",
813
824
  position: "",
814
825
  createdAt: 0,
815
826
  updatedAt: 0
@@ -1,6 +1,7 @@
1
1
  import { LayoutRequestContext } from "./requestContext.js";
2
+ import { KnownPropertiesOnly, RecordNode } from "./structuredData.js";
2
3
  import { LayoutCollectionItemResult, LayoutRoutesResult, RecordPages } from "./createLayout.js";
3
- import { CollectionFieldBuilder, StringField } from "./lib/contentType.js";
4
+ import { CollectionFieldBuilder, RecordStructuredDataContext, RecordTemplateContext } from "./lib/contentType.js";
4
5
  import { SystemOrderKey } from "@camox/api-contract";
5
6
  import { Static, TObject, TSchema } from "@sinclair/typebox";
6
7
  //#region src/core/createCollection.d.ts
@@ -11,15 +12,27 @@ type TextKey<T extends Record<string, TSchema>> = { [K in keyof T & string]: T[K
11
12
  } ? K : never; }[keyof T & string];
12
13
  /**
13
14
  * A definition only: record authoring and querying are not public SDK APIs.
14
- * `label` is checked against `content`'s return type: typing it against `T` directly would fix
15
- * `T` before the `content` callback is inferred.
15
+ *
16
+ * `toTitle` and `toDescription` are templates of the record's String fields, such as
17
+ * `` ({ content }) => `${content.firstName} ${content.lastName}` ``. The API fills them with each
18
+ * record's plain text wherever records are named: studio lists and pickers, record pages,
19
+ * `llms.txt` and agents. `toDescription` may return nothing, and records then have no description.
16
20
  */
17
- export declare function createCollection<const T extends Record<string, TSchema> & ReservedFieldNames, const L extends string>(options: {
21
+ export declare function createCollection<const T extends Record<string, TSchema> & ReservedFieldNames, Node extends RecordNode = RecordNode>(options: {
18
22
  id: string;
19
23
  title: string;
20
24
  description: string;
21
- content: (field: CollectionFieldBuilder) => T & Record<NoInfer<L>, StringField>;
22
- label: L;
25
+ content: (field: CollectionFieldBuilder) => T;
26
+ toTitle: (context: RecordTemplateContext<NoInfer<T>>) => string;
27
+ toDescription: (context: RecordTemplateContext<NoInfer<T>>) => string | undefined;
28
+ /**
29
+ * Each record's Structured Data node, from tokens of its fields, such as
30
+ * `({ content }) => ({ "@type": "Person", jobTitle: content.role })`. Camox fills its name and
31
+ * description from the record's title and description, and gives it an `@id` per record and
32
+ * locale. Every page referencing the record links this one node, and on the record's own page
33
+ * it's the main entity unless the layout declares one. A reference links the record's node.
34
+ */
35
+ toStructuredData?: (context: RecordStructuredDataContext<NoInfer<T>>) => Node & KnownPropertiesOnly<Node, RecordNode>;
23
36
  }): Collection<T>;
24
37
  /** The pages of a derived layout, one per record, found by the record's `by` field. */
25
38
  export interface CollectionPages<T extends Record<string, TSchema>, Param extends string> {
@@ -47,7 +60,12 @@ export type Collection<T extends Record<string, TSchema> = any> = {
47
60
  id: string;
48
61
  title: string;
49
62
  description: string;
50
- label: TextKey<T>;
63
+ /** The token template titling each record, such as `{{firstName}} {{lastName}}`. */
64
+ toTitle: string;
65
+ /** The token template describing each record, or null when records have no description. */
66
+ toDescription: string | null;
67
+ /** The JSON template of each record's Structured Data node, or null when records have none. */
68
+ toStructuredData: Record<string, unknown> | null;
51
69
  contentSchema: TObject<T>;
52
70
  };
53
71
  };
@@ -1,14 +1,17 @@
1
- import { collectionFieldBuilder } from "./lib/contentType.js";
1
+ import { collectionFieldBuilder, resolveRecordStructuredData, resolveRecordTemplate } from "./lib/contentType.js";
2
2
  import { notFound } from "./notFound.js";
3
3
  import { getRecordLookup } from "./requestContext.js";
4
- import { SYSTEM_ORDER_KEYS } from "@camox/api-contract";
4
+ import { SYSTEM_ORDER_KEYS, nonStringTemplateFields } from "@camox/api-contract";
5
5
  import { Type } from "@sinclair/typebox";
6
6
 
7
7
  //#region src/core/createCollection.ts
8
8
  /**
9
9
  * A definition only: record authoring and querying are not public SDK APIs.
10
- * `label` is checked against `content`'s return type: typing it against `T` directly would fix
11
- * `T` before the `content` callback is inferred.
10
+ *
11
+ * `toTitle` and `toDescription` are templates of the record's String fields, such as
12
+ * `` ({ content }) => `${content.firstName} ${content.lastName}` ``. The API fills them with each
13
+ * record's plain text wherever records are named: studio lists and pickers, record pages,
14
+ * `llms.txt` and agents. `toDescription` may return nothing, and records then have no description.
12
15
  */
13
16
  function createCollection(options) {
14
17
  if (!options.id.trim()) throw new Error("Collection id is required");
@@ -27,7 +30,13 @@ function createCollection(options) {
27
30
  "ReferenceList"
28
31
  ]);
29
32
  for (const [key, field] of Object.entries(content)) if (!supported.has(field.fieldType)) throw new Error(`Collection "${options.id}" field "${key}": ${String(field.fieldType)} storage is not supported yet`);
30
- if (content[options.label]?.fieldType !== "String") throw new Error("Collection label must name a String field");
33
+ const toTitle = resolveRecordTemplate(options.toTitle, content);
34
+ if (toTitle === null) throw new Error(`Collection "${options.id}": toTitle returned nothing`);
35
+ const toDescription = resolveRecordTemplate(options.toDescription, content);
36
+ for (const template of [toTitle, toDescription]) {
37
+ const [name] = nonStringTemplateFields(template ?? "", content);
38
+ if (name !== void 0) throw new Error(`Collection "${options.id}": "${name}" names no String field`);
39
+ }
31
40
  const id = options.id;
32
41
  return {
33
42
  pages({ by, param }) {
@@ -49,10 +58,17 @@ function createCollection(options) {
49
58
  collectionId: id,
50
59
  data: {
51
60
  id: record.id,
61
+ ...record.title !== void 0 && { title: record.title },
62
+ ...record.description !== void 0 && { description: record.description },
52
63
  content: record.content,
53
64
  ...record.references && { references: record.references },
54
65
  ...record.updatedAt != null && { updatedAt: record.updatedAt }
55
- }
66
+ },
67
+ ...record.structuredData && { structuredData: record.structuredData },
68
+ ...record.firstPublishedAt !== void 0 && { published: {
69
+ first: record.firstPublishedAt,
70
+ last: record.publishedAt ?? null
71
+ } }
56
72
  };
57
73
  },
58
74
  async routes({ context }) {
@@ -69,13 +85,14 @@ function createCollection(options) {
69
85
  kind: "collection-item",
70
86
  collectionId: id,
71
87
  id: record.id,
72
- label: record.label
88
+ title: record.title
73
89
  }
74
90
  })) };
75
91
  },
76
92
  recordPages: {
77
93
  collectionId: id,
78
- field: by
94
+ field: by,
95
+ param
79
96
  }
80
97
  };
81
98
  },
@@ -83,7 +100,9 @@ function createCollection(options) {
83
100
  id,
84
101
  title: options.title,
85
102
  description: options.description,
86
- label: options.label,
103
+ toTitle,
104
+ toDescription,
105
+ toStructuredData: options.toStructuredData ? resolveRecordStructuredData(options.toStructuredData, content) : null,
87
106
  contentSchema: Type.Object(content, { additionalProperties: false })
88
107
  }
89
108
  };