camox 0.47.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 (75) 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 +1 -1
  25. package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
  26. package/dist/features/content/ContentCollection.js +13 -7
  27. package/dist/features/content/collection-form.js +6 -2
  28. package/dist/features/content/components/MissingFromLocale.js +1 -1
  29. package/dist/features/og/pageImage.js +89 -0
  30. package/dist/features/page/CuratedPageContent.js +45 -32
  31. package/dist/features/page/DerivedPageContent.js +49 -22
  32. package/dist/features/page/SharedLayoutContent.js +26 -22
  33. package/dist/features/preview/CamoxPreview.js +40 -26
  34. package/dist/features/preview/components/AddBlockSheet.js +1 -0
  35. package/dist/features/preview/components/BlockActionsPopover.js +8 -6
  36. package/dist/features/preview/components/PageInfo.js +244 -264
  37. package/dist/features/preview/components/PageTree.js +63 -53
  38. package/dist/features/preview/components/RecordCard.js +50 -34
  39. package/dist/features/preview/components/RecordCombobox.js +12 -5
  40. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
  41. package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
  42. package/dist/features/preview/components/RepeatableItemsList.js +62 -46
  43. package/dist/features/preview/components/SelectionEditor.js +3 -2
  44. package/dist/features/preview/components/useFieldWriter.js +2 -4
  45. package/dist/features/preview/components/useRecordView.js +2 -2
  46. package/dist/features/preview/entityName.js +11 -0
  47. package/dist/features/preview/pageRecord.js +17 -18
  48. package/dist/features/routes/pageRuntime.d.ts +15 -1
  49. package/dist/features/routes/pageRuntime.js +35 -38
  50. package/dist/features/runtime/RuntimeChrome.js +1 -1
  51. package/dist/features/runtime/breadcrumbs.js +68 -0
  52. package/dist/features/runtime/livePages.js +3 -2
  53. package/dist/features/runtime/pageGraph.js +92 -0
  54. package/dist/features/runtime/projectLogo.js +110 -0
  55. package/dist/features/runtime/runtime.d.ts +11 -6
  56. package/dist/features/runtime/runtime.js +278 -55
  57. package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
  58. package/dist/features/runtime/runtimeRoutes.js +1 -0
  59. package/dist/features/runtime/structuredData.js +213 -0
  60. package/dist/features/studio/components/ChatSidebar.js +5 -5
  61. package/dist/features/studio/components/ProjectMenu.js +127 -100
  62. package/dist/features/studio/components/SocialProfilesDialog.js +342 -0
  63. package/dist/features/studio/components/ThreadMessages.js +2 -2
  64. package/dist/features/vite/blockBoilerplate.js +3 -2
  65. package/dist/features/vite/layoutGeneration.js +3 -2
  66. package/dist/features/vite/vite.js +1 -0
  67. package/dist/lib/queries.d.ts +4 -0
  68. package/dist/lib/queries.js +1 -0
  69. package/dist/lib/studio-reads.js +3 -1
  70. package/dist/package.js +1 -1
  71. package/dist/studio.css +1 -1
  72. package/package.json +5 -4
  73. package/skills/camox/references/block-definitions.md +105 -25
  74. package/skills/camox/references/layout-definitions.md +53 -17
  75. package/skills/camox/references/routed-layouts.md +17 -14
@@ -1,15 +1,13 @@
1
- import { ReferenceScope } from "./lib/reference.js";
2
- import { CollectionRecordId, LayoutRequestContext, LoadedRecord } from "./requestContext.js";
1
+ import { AnyLayoutContext, CuratedLayoutContext, LayoutPage, LayoutProject, LoadedLayoutContext, RecordLayoutContext } from "./layoutContext.js";
2
+ import { ReferenceKeys, ReferenceListKeys, ReferenceScope } from "./lib/reference.js";
3
+ import { CollectionRecordId, LayoutRequestContext, LoadedRecord, RecordPageStructuredData } from "./requestContext.js";
4
+ import { KnownPropertiesOnly, LayoutStructuredData, MainEntity, StructuredDataNode, StructuredDataType, WebPageKind } from "./structuredData.js";
3
5
  import { notFound } from "./notFound.js";
4
6
  import { ContentProxy } from "./lib/contentType.js";
5
7
  import * as React from "react";
8
+ import { RecordLink } from "@camox/api-contract";
6
9
  import { TSchema } from "@sinclair/typebox";
7
10
  //#region src/core/createLayout.d.ts
8
- export interface OgImageParams {
9
- title: string;
10
- description: string;
11
- projectName: string;
12
- }
13
11
  export interface LayoutBlockData {
14
12
  _id: string;
15
13
  type: string;
@@ -74,6 +72,13 @@ export interface LayoutCollectionItemResult<Content = Record<string, unknown>, C
74
72
  kind: "collection-item";
75
73
  collectionId: CollectionId;
76
74
  data: LoadedRecord<Content>;
75
+ /** The record's Structured Data, for its page's graph. Set by `collection.pages()`. */
76
+ structuredData?: RecordPageStructuredData;
77
+ /** When the record was first and last published to Live, for its page's graph. */
78
+ published?: {
79
+ first: number | null;
80
+ last: number | null;
81
+ };
77
82
  /**
78
83
  * Type only, never set: the collection's fields, which type `layout.Item`. Invariant, so that
79
84
  * records of two collections never merge into one.
@@ -103,8 +108,8 @@ type LoaderReturn = AnyLayoutLoaderResult | PromiseLike<AnyLayoutLoaderResult>;
103
108
  /** The record a discovered page renders, independent of its current params. */
104
109
  export interface LayoutRouteTarget extends CollectionRecordId {
105
110
  kind: "collection-item";
106
- /** The record's Label, which names its page where pages are listed (llms.txt). */
107
- label?: string;
111
+ /** The record's title, which names its page where pages are listed (llms.txt). */
112
+ title?: string;
108
113
  }
109
114
  /** Route discovery result: the params of each page a derived layout can render. */
110
115
  export interface LayoutRoutesResult<Params = Record<string, string>> {
@@ -126,10 +131,6 @@ type RecordFields<Result> = [Awaited<Result>] extends [never] ? never : [Awaited
126
131
  type StringFieldKeys<F> = { [K in keyof F & string]: F[K] extends {
127
132
  fieldType: "String";
128
133
  } ? K : never; }[keyof F & string];
129
- /** Record pages' meta callbacks also get the record's String fields as plain text. */
130
- type MetaItem<F> = [F] extends [never] ? {} : {
131
- item: { [K in StringFieldKeys<F>]: string; };
132
- };
133
134
  /** Record pages' `toMarkdown` also gets `item`, the reference-list Markdown builder. */
134
135
  type MarkdownItem<F> = [F] extends [never] ? {} : F extends Record<string, TSchema> ? {
135
136
  item: ContentProxy<F>;
@@ -141,39 +142,51 @@ type ItemScope<F> = [F] extends [never] ? {} : F extends Record<string, TSchema>
141
142
  children: (item: ReferenceScope<F>) => React.ReactNode;
142
143
  }) => React.ReactNode;
143
144
  } : {};
144
- /** What a page's metadata callbacks know about it. Curated pages have no loader data or params. */
145
- export interface PageMetaContext<Data = unknown, Params = Record<string, string>> {
146
- projectName: string;
147
- pageFullPath: string;
148
- data: Data;
149
- params: Params;
150
- }
151
- /** On singleton and derived pages, `pageMetaTitle` is the layout's `title`. */
152
- type BuildMetaTitle<Data, Params, F = never> = (params: PageMetaContext<Data, Params> & {
153
- pageMetaTitle: string;
154
- } & MetaItem<F>) => string;
155
- /** Without it, curated pages use the editor's description and other pages have none. */
156
- type BuildMetaDescription<Data, Params, F = never> = (params: PageMetaContext<Data, Params> & {
157
- pageMetaDescription: string | undefined;
158
- } & MetaItem<F>) => string | undefined;
159
145
  /** The Markdown Representation of a code-owned page, between its layout blocks. */
160
146
  type LayoutToMarkdown<Data, Params, F = never> = (context: {
161
147
  data: Data;
162
148
  params: Params;
163
149
  } & MarkdownItem<F>) => readonly string[];
164
- /** The collection a layout's record pages render, and the field each record is found by. */
150
+ /**
151
+ * On record pages, the context's `content` holds the record's String fields as plain text, and
152
+ * its references as links to their records' nodes, for `toStructuredData`.
153
+ */
154
+ type RecordContent<F> = [F] extends [never] ? {} : F extends Record<string, TSchema> ? {
155
+ content: { [K in StringFieldKeys<F>]: string; } & { [K in ReferenceKeys<F>]: RecordLink | undefined; } & { [K in ReferenceListKeys<F>]: RecordLink[]; };
156
+ } : {
157
+ content: { [K in StringFieldKeys<F>]: string; };
158
+ };
159
+ /** The context a singleton or derived layout's `to*` options receive. */
160
+ type LoadedContext<Data, Params, F = never> = LoadedLayoutContext<Data, Params> & RecordContent<F>;
161
+ /**
162
+ * A layout's outputs, each receiving the page's context: `toReact`, `toTitle`, `toDescription`,
163
+ * `toImage` and `toStructuredData`.
164
+ */
165
+ type LayoutOutputs<Context, Children, Declared extends LayoutStructuredData | undefined> = {
166
+ toReact: React.ComponentType<Context & Children>;
167
+ toTitle: (context: Context) => string;
168
+ /** Required, but may return nothing, such as `({ page }) => page.description`. */
169
+ toDescription: (context: Context) => string | undefined;
170
+ toImage?: (context: Context) => React.ReactElement;
171
+ /**
172
+ * What the page is: a kind of web page (`AboutPage`, `ContactPage`, …), which its node
173
+ * becomes, or a main entity (`BlogPosting`, `Product`, `Event`, …), which Camox emits as the
174
+ * page's `mainEntity`. Camox fills the main entity's name or headline, description, URL,
175
+ * image, dates, publisher and language where the layout leaves them out.
176
+ */
177
+ toStructuredData?: (context: Context) => Declared & KnownPropertiesOnly<Declared, LayoutStructuredData>;
178
+ };
179
+ /**
180
+ * The collection a layout's record pages render, and the field each record is found by. `param`
181
+ * names the route param holding that field's value; only the collection and field are synced.
182
+ */
165
183
  export type RecordPages = {
166
184
  collectionId: string;
167
185
  field: string;
186
+ param?: string;
168
187
  };
169
- interface CreateLayoutOptions<TBefore extends readonly LayoutBlock[], TAfter extends readonly LayoutBlock[], TInitial extends readonly InitialBlockImport[], Result extends LoaderReturn = LoaderReturn> {
188
+ interface LayoutBaseOptions<TBefore extends readonly LayoutBlock[], TAfter extends readonly LayoutBlock[], TInitial extends readonly InitialBlockImport[]> {
170
189
  id: string;
171
- kind?: "curated" | "derived" | "singleton";
172
- loader?: (context: LayoutLoaderContext) => Result;
173
- /** Lists the pages of a derived layout, for the sitemap and the Canvas. */
174
- routes?: (context: LayoutRoutesContext) => RoutesReturn<Record<string, string>>;
175
- /** A derived layout's record pages, set by spreading `collection.pages(...)`. */
176
- recordPages?: RecordPages;
177
190
  title: string;
178
191
  description: string;
179
192
  blocks: {
@@ -185,16 +198,33 @@ interface CreateLayoutOptions<TBefore extends readonly LayoutBlock[], TAfter ext
185
198
  */
186
199
  initial?: ValidatePageContentBlocks<TInitial>;
187
200
  };
188
- component: React.ComponentType<{
189
- children: React.ReactNode;
190
- }>;
191
- buildMetaTitle: BuildMetaTitle<LoaderData<Result>, Record<string, string>, RecordFields<Result>>;
192
- buildMetaDescription?: BuildMetaDescription<LoaderData<Result>, Record<string, string>, RecordFields<Result>>;
201
+ }
202
+ type CreateLayoutOptions<TBefore extends readonly LayoutBlock[], TAfter extends readonly LayoutBlock[], TInitial extends readonly InitialBlockImport[], Result extends LoaderReturn = LoaderReturn, Declared extends LayoutStructuredData | undefined = LayoutStructuredData | undefined> = LayoutBaseOptions<TBefore, TAfter, TInitial> & {
203
+ kind?: "curated" | "derived" | "singleton";
204
+ loader?: (context: LayoutLoaderContext) => Result;
205
+ /** Lists the pages of a derived layout, for the sitemap and the Canvas. */
206
+ routes?: (context: LayoutRoutesContext) => RoutesReturn<Record<string, string>>;
207
+ /** A derived layout's record pages, set by spreading `collection.pages(...)`. */
208
+ recordPages?: RecordPages;
193
209
  /** Singleton and derived layouts only. */
194
210
  toMarkdown?: LayoutToMarkdown<LoaderData<Result>, Record<string, string>, RecordFields<Result>>;
195
- buildOgImage?: (params: OgImageParams) => React.ReactElement;
196
- }
197
- declare function createLayoutDefinition<const TBefore extends readonly LayoutBlock[], const TAfter extends readonly LayoutBlock[], const TInitial extends readonly InitialBlockImport[] = []>(options: CreateLayoutOptions<TBefore, TAfter, TInitial>): {
211
+ } & LayoutOutputs<LoadedContext<LoaderData<Result>, Record<string, string>, RecordFields<Result>>, {
212
+ children: React.ReactNode;
213
+ }, Declared>;
214
+ /** Every option a layout may define, as the implementation reads them. */
215
+ type AnyLayoutOptions = LayoutBaseOptions<any, any, any> & {
216
+ kind?: "curated" | "derived" | "singleton";
217
+ loader?: (context: LayoutLoaderContext) => LoaderReturn;
218
+ routes?: (context: LayoutRoutesContext) => RoutesReturn<Record<string, string>>;
219
+ recordPages?: RecordPages;
220
+ toMarkdown?: LayoutToMarkdown<any, any, any>;
221
+ toReact: React.ComponentType<any>;
222
+ toTitle: (context: any) => string;
223
+ toDescription: (context: any) => string | undefined;
224
+ toImage?: (context: any) => React.ReactElement;
225
+ toStructuredData?: (context: any) => LayoutStructuredData | undefined;
226
+ };
227
+ declare function createLayoutDefinition(options: AnyLayoutOptions): {
198
228
  BeforeBlocks: {
199
229
  (): React.JSX.Element;
200
230
  displayName: string;
@@ -218,14 +248,19 @@ declare function createLayoutDefinition<const TBefore extends readonly LayoutBlo
218
248
  recordPages: RecordPages | undefined;
219
249
  title: string;
220
250
  description: string;
221
- buildMetaTitle: BuildMetaTitle<unknown, unknown>;
222
- buildMetaDescription: BuildMetaDescription<unknown, unknown> | undefined;
251
+ /** The page's title, from the page's context. */
252
+ toTitle: (context: AnyLayoutContext) => string;
253
+ /** The page's description, from the page's context. */
254
+ toDescription: (context: AnyLayoutContext) => string | undefined;
223
255
  toMarkdown: ((context: {
224
256
  data: unknown;
225
257
  params: unknown;
226
258
  item?: unknown;
227
259
  }) => readonly string[]) | undefined;
228
- buildOgImage: ((params: OgImageParams) => Promise<Response>) | undefined;
260
+ /** The page's shared image, which `/og` renders. Without one, Camox's default image. */
261
+ toImage: ((context: AnyLayoutContext) => React.ReactElement) | undefined;
262
+ /** What the page is, from the page's context. Without it, a plain web page. */
263
+ toStructuredData: (context: AnyLayoutContext) => LayoutStructuredData | undefined;
229
264
  blockDefinitions: ({
230
265
  type: string;
231
266
  content: Record<string, unknown>;
@@ -263,8 +298,9 @@ declare function createLayoutDefinition<const TBefore extends readonly LayoutBlo
263
298
  position: string;
264
299
  }[];
265
300
  }[]>) | undefined;
266
- component: React.ComponentType<{
267
- children: React.ReactNode;
301
+ /** Renders the layout around the page, with the page's context as props. */
302
+ toReact: React.ComponentType<AnyLayoutContext & {
303
+ children?: React.ReactNode;
268
304
  }>;
269
305
  Provider: ({ layoutBlocks, children, result }: {
270
306
  layoutBlocks: Record<string, LayoutBlockData>;
@@ -284,18 +320,15 @@ type Params<Id extends string> = Id extends `${infer Head}.${infer Tail}` ? Para
284
320
  type RouteParams<Id extends string> = Id extends keyof LayoutFileRoutes ? LayoutFileRoutes[Id] extends {
285
321
  params: infer P;
286
322
  } ? P : Params<Id> : Params<Id>;
287
- type FileOptions<Id extends string, Result extends LoaderReturn, B extends readonly LayoutBlock[], A extends readonly LayoutBlock[], I extends readonly InitialBlockImport[]> = Omit<CreateLayoutOptions<B, A, I>, "id" | "kind" | "loader" | "routes" | "recordPages" | "component" | "buildMetaTitle" | "buildMetaDescription" | "toMarkdown"> & ({
323
+ type FileOptions<Id extends string, Result extends LoaderReturn, B extends readonly LayoutBlock[], A extends readonly LayoutBlock[], I extends readonly InitialBlockImport[], Declared extends LayoutStructuredData | undefined> = Omit<LayoutBaseOptions<B, A, I>, "id"> & (({
288
324
  kind: "curated";
289
- component: React.ComponentType<{
290
- children: React.ReactNode;
291
- }>;
292
325
  loader?: never;
293
326
  routes?: never;
294
327
  recordPages?: never;
295
328
  toMarkdown?: never;
296
- buildMetaTitle: BuildMetaTitle<undefined, Record<string, never>>;
297
- buildMetaDescription?: BuildMetaDescription<undefined, Record<string, never>>;
298
- } | {
329
+ } & LayoutOutputs<CuratedLayoutContext, {
330
+ children: React.ReactNode;
331
+ }, Declared>) | ({
299
332
  kind: "singleton";
300
333
  blocks: {
301
334
  before: ValidateLayoutOnlyBlocks<B>;
@@ -306,12 +339,9 @@ type FileOptions<Id extends string, Result extends LoaderReturn, B extends reado
306
339
  routes?: never;
307
340
  recordPages?: never;
308
341
  toMarkdown?: LayoutToMarkdown<LoaderData<Result>, Record<string, never>, RecordFields<Result>>;
309
- component: React.ComponentType<{
310
- children?: never;
311
- }>;
312
- buildMetaTitle: BuildMetaTitle<LoaderData<Result>, Record<string, never>, RecordFields<Result>>;
313
- buildMetaDescription?: BuildMetaDescription<LoaderData<Result>, Record<string, never>, RecordFields<Result>>;
314
- } | {
342
+ } & LayoutOutputs<LoadedContext<LoaderData<Result>, Record<string, never>, RecordFields<Result>>, {
343
+ children?: never;
344
+ }, Declared>) | ({
315
345
  kind: "derived";
316
346
  blocks: {
317
347
  before: ValidateLayoutOnlyBlocks<B>;
@@ -322,13 +352,10 @@ type FileOptions<Id extends string, Result extends LoaderReturn, B extends reado
322
352
  routes?: (context: LayoutRoutesContext) => RoutesReturn<RouteParams<Id>>;
323
353
  recordPages?: RecordPages;
324
354
  toMarkdown?: LayoutToMarkdown<LoaderData<Result>, RouteParams<Id>, RecordFields<Result>>;
325
- component: React.ComponentType<{
326
- children?: never;
327
- }>;
328
- buildMetaTitle: BuildMetaTitle<LoaderData<Result>, RouteParams<Id>, RecordFields<Result>>;
329
- buildMetaDescription?: BuildMetaDescription<LoaderData<Result>, RouteParams<Id>, RecordFields<Result>>;
330
- });
331
- export declare function createLayout<const B extends readonly LayoutBlock[], const A extends readonly LayoutBlock[], const I extends readonly InitialBlockImport[] = [], Result extends LoaderReturn = never>(options: CreateLayoutOptions<B, A, I, Result>): LayoutFor<Result>;
332
- export declare function createLayout<const Id extends string>(id: Id): <Result extends LoaderReturn = never, const B extends readonly LayoutBlock[] = [], const A extends readonly LayoutBlock[] = [], const I extends readonly InitialBlockImport[] = []>(options: FileOptions<Id, Result, B, A, I>) => LayoutFor<Result>;
355
+ } & LayoutOutputs<LoadedContext<LoaderData<Result>, RouteParams<Id>, RecordFields<Result>>, {
356
+ children?: never;
357
+ }, Declared>));
358
+ export declare function createLayout<const B extends readonly LayoutBlock[], const A extends readonly LayoutBlock[], const I extends readonly InitialBlockImport[] = [], Result extends LoaderReturn = never, Declared extends LayoutStructuredData | undefined = undefined>(options: CreateLayoutOptions<B, A, I, Result, Declared>): LayoutFor<Result>;
359
+ export declare function createLayout<const Id extends string>(id: Id): <Result extends LoaderReturn = never, const B extends readonly LayoutBlock[] = [], const A extends readonly LayoutBlock[] = [], const I extends readonly InitialBlockImport[] = [], Declared extends LayoutStructuredData | undefined = undefined>(options: FileOptions<Id, Result, B, A, I, Declared>) => LayoutFor<Result>;
333
360
  //#endregion
334
- export { type LayoutRequestContext, notFound };
361
+ export { type CuratedLayoutContext, type LayoutPage, type LayoutProject, type LayoutRequestContext, type LayoutStructuredData, type LoadedLayoutContext, type MainEntity, type RecordLayoutContext, type StructuredDataNode, type StructuredDataType, type WebPageKind, notFound };
@@ -2,7 +2,7 @@ import { notFound } from "./notFound.js";
2
2
  import { useOptionalCamoxApp } from "../features/provider/components/CamoxAppContext.js";
3
3
  import { useBlockEditingRuntime } from "./editing/BlockEditingRuntime.js";
4
4
  import { BlockErrorBoundary } from "../features/preview/components/BlockErrorBoundary.js";
5
- import { asCollectionItem, fieldPlainText } from "./recordItem.js";
5
+ import { asCollectionItem } from "./recordItem.js";
6
6
  import { RecordScope } from "./lib/recordScope.js";
7
7
  import { c } from "react/compiler-runtime";
8
8
  import * as React from "react";
@@ -133,12 +133,13 @@ function createLayoutDefinition(options) {
133
133
  t2 = null;
134
134
  break bb0;
135
135
  }
136
- const { contentSchema, label } = collection._internal;
136
+ const { contentSchema } = collection._internal;
137
137
  const { id, content, references } = result.data;
138
138
  const readOnly = /* @__PURE__ */ jsx(RecordScope, {
139
139
  record: {
140
140
  id,
141
- label: fieldPlainText(content[label]),
141
+ title: result.data.title ?? "",
142
+ description: result.data.description ?? null,
142
143
  content,
143
144
  references
144
145
  },
@@ -152,7 +153,7 @@ function createLayoutDefinition(options) {
152
153
  }
153
154
  t1 = editingRuntime.renderLayoutItem({
154
155
  collectionId: result.collectionId,
155
- labelField: label,
156
+ collection,
156
157
  properties: contentSchema.properties,
157
158
  record: result.data,
158
159
  children,
@@ -192,14 +193,6 @@ function createLayoutDefinition(options) {
192
193
  slot: "after"
193
194
  };
194
195
  })];
195
- const buildOgImage = options.buildOgImage ? async (params) => {
196
- if (import.meta.env?.SSR === false) throw new Error("OG images can only be rendered on the server.");
197
- const { ImageResponse } = await import("camox/_internal/imageResponse");
198
- return new ImageResponse(options.buildOgImage(params), {
199
- width: 1200,
200
- height: 630
201
- });
202
- } : void 0;
203
196
  const loadInitialBlockBundles = initialBlocks && (() => Promise.all(initialBlocks.map(async (load) => {
204
197
  const { block } = await load();
205
198
  if (block._internal.layoutOnly) throw new Error(`${INITIAL_LAYOUT_ONLY_ERROR} (block "${block._internal.id}")`);
@@ -228,13 +221,14 @@ function createLayoutDefinition(options) {
228
221
  recordPages: options.recordPages,
229
222
  title: options.title,
230
223
  description: options.description,
231
- buildMetaTitle: options.buildMetaTitle,
232
- buildMetaDescription: options.buildMetaDescription,
224
+ toTitle: (context) => options.toTitle(context),
225
+ toDescription: (context) => options.toDescription(context),
233
226
  toMarkdown: options.toMarkdown,
234
- buildOgImage,
227
+ toImage: options.toImage,
228
+ toStructuredData: (context) => options.toStructuredData?.(context),
235
229
  blockDefinitions,
236
230
  loadInitialBlockBundles,
237
- component: options.component,
231
+ toReact: options.toReact,
238
232
  Provider
239
233
  }
240
234
  };
@@ -55,7 +55,7 @@ function targetRecord(entry) {
55
55
  if (typeof target?.id !== "string") return {};
56
56
  return {
57
57
  recordId: target.id,
58
- ...typeof target.label === "string" && { label: target.label }
58
+ ...typeof target.title === "string" && { title: target.title }
59
59
  };
60
60
  }
61
61
  /**
@@ -1,11 +1,12 @@
1
1
  import { LoadedRecord } from "../requestContext.js";
2
+ import { Collection } from "../createCollection.js";
2
3
  import * as React from "react";
3
4
  //#region src/core/editing/BlockEditingRuntime.d.ts
4
5
  /** A record page's record, as `layout.Item` hands it to the editing runtime. */
5
6
  export interface LayoutItemProps {
6
7
  collectionId: string;
7
- /** The collection's label field. */
8
- labelField: string;
8
+ /** The record's collection, whose templates title and describe it. */
9
+ collection: Collection;
9
10
  /** The collection's field schemas. */
10
11
  properties: Record<string, unknown>;
11
12
  record: LoadedRecord;
@@ -40,7 +40,8 @@ const layoutItemOptions = {
40
40
  description: "",
41
41
  content: () => ({}),
42
42
  toMarkdown: () => [],
43
- component: () => null
43
+ toReact: () => null,
44
+ toTitle: () => ""
44
45
  };
45
46
  const completeEditingRuntime = {
46
47
  renderLayoutItem(props) {
@@ -3,13 +3,14 @@ import { ArtifactRenderProps } from "../lib/artifact.js";
3
3
  import { InlineStyle, InlineTextStyles, TextLinkStyleData, TextStyleData } from "../lib/textStyles.js";
4
4
  import "../lib/lexicalReact.js";
5
5
  import { ReferenceContent, ReferenceKeys, ReferenceListKeys, ReferenceScope } from "../lib/reference.js";
6
+ import { BlockStructuredData } from "../structuredData.js";
6
7
  import { IconValue } from "../lib/iconTypes.js";
7
- import { ArtifactDataCheck, ArtifactSource, ContentFieldBuilder, EmbedURL, FileValue, ImageValue, ItemSettingsBrand, LinkValue, SettingBuilder, ToMarkdownBuilder } from "../lib/contentType.js";
8
+ import { ArtifactDataCheck, ArtifactSource, ContentFieldBuilder, EmbedURL, FileValue, ImageValue, ItemSettingsBrand, LinkValue, SettingBuilder, ToMarkdownBuilder, ToStructuredDataBuilder, ToTitleBuilder } from "../lib/contentType.js";
8
9
  import { LayoutItemProps } from "./BlockEditingRuntime.js";
9
10
  import * as React from "react";
10
11
  import { Static, TArray, TObject, TSchema, Type } from "@sinclair/typebox";
11
12
  //#region src/core/editing/createEditableBlock.d.ts
12
- interface CreateBlockOptions<TSchemaShape extends Record<string, TSchema> = Record<string, TSchema>, TSettingsShape extends Record<string, TSchema> = Record<string, TSchema>, TLayoutOnly extends boolean = false> {
13
+ interface BlockBaseOptions<TSchemaShape extends Record<string, TSchema>, TSettingsShape extends Record<string, TSchema>, TLayoutOnly extends boolean, TNodes extends BlockStructuredData = BlockStructuredData> {
13
14
  id: string;
14
15
  /**
15
16
  * Human-readable title for the block (JSON Schema `title`).
@@ -31,7 +32,8 @@ interface CreateBlockOptions<TSchemaShape extends Record<string, TSchema> = Reco
31
32
  * content: (field) => ({ name: field.string({ default: "Item" }) }),
32
33
  * minItems: 1,
33
34
  * maxItems: 10,
34
- * toMarkdown: (c) => [c.name],
35
+ * toTitle: ({ content }) => content.name,
36
+ * toMarkdown: ({ content }) => [content.name],
35
37
  * }),
36
38
  * })
37
39
  */
@@ -54,28 +56,62 @@ interface CreateBlockOptions<TSchemaShape extends Record<string, TSchema> = Reco
54
56
  layoutOnly?: TLayoutOnly;
55
57
  /** Share content and settings across all placements of this block definition. Defaults to false. */
56
58
  synced?: boolean;
57
- /**
58
- * React component that renders the block.
59
- * Must be defined as a separate function (not inline, not an arrow function).
60
- * Should use the Field component returned by createBlock to render editable content.
61
- */
62
- component: React.ComponentType<{
63
- content: Static<ReturnType<typeof Type.Object<TSchemaShape>>>;
64
- }>;
65
59
  /**
66
60
  * Builder for rendering block content as markdown.
67
- * `c` is a proxy typed on `content` keys — `c.title`, `c.description`, etc.
68
- * `s` is a proxy typed on `settings` keys — call a boolean setting with a line
69
- * (`s.showCta(c.cta)`) or an enum with a value (`s.variant("banner", [...])`)
70
- * to wrap lines in a conditional. Each returned entry becomes a paragraph
71
- * (joined with `\n\n`). Lines where all referenced fields resolve to empty
61
+ * `content` is a proxy typed on `content` keys — `content.title`, `content.description`, etc.
62
+ * `settings` is a proxy typed on `settings` keys — call a boolean setting with a line
63
+ * (`settings.showCta(content.cta)`) or an enum with a value
64
+ * (`settings.variant("banner", [...])`) to wrap lines in a conditional. Each returned entry
65
+ * becomes a paragraph (joined with `\n\n`). Lines where all referenced fields resolve to empty
72
66
  * (or whose condition is false) are omitted at render time.
73
67
  *
74
68
  * @example
75
- * toMarkdown: (c, s) => [`# ${c.title}`, c.description, s.showCta(c.cta)]
69
+ * toMarkdown: ({ content, settings }) => [
70
+ * `# ${content.title}`,
71
+ * content.description,
72
+ * settings.showCta(content.cta),
73
+ * ]
76
74
  */
77
75
  toMarkdown: ToMarkdownBuilder<TSchemaShape, TSettingsShape>;
76
+ /**
77
+ * Names the block in the studio and to agents until it has a Summary: one line from
78
+ * `{ content, settings }`, read like `toMarkdown`'s, on the default locale's content.
79
+ *
80
+ * @example
81
+ * toTitle: ({ content }) => content.title
82
+ */
83
+ toTitle: ToTitleBuilder<TSchemaShape, TSettingsShape>;
84
+ /**
85
+ * The things the block shows, as Structured Data nodes from `{ content, settings }`: one node,
86
+ * or a list. A repeater's token places its items' nodes. Camox links each node to the page,
87
+ * and each placement gets its own. Blocks never say what the page is, and can't describe an
88
+ * `Organization`: Camox emits the site's.
89
+ *
90
+ * @example
91
+ * toStructuredData: ({ content }) => ({
92
+ * "@type": "VideoObject",
93
+ * name: content.title,
94
+ * embedUrl: content.video,
95
+ * thumbnailUrl: content.thumbnail,
96
+ * uploadDate: content.uploadDate,
97
+ * })
98
+ */
99
+ toStructuredData?: ToStructuredDataBuilder<TSchemaShape, TSettingsShape, TNodes>;
78
100
  }
101
+ /** What a block's `toReact` receives as props: its content and settings. */
102
+ export interface BlockContext<TSchemaShape extends Record<string, TSchema>, TSettingsShape extends Record<string, TSchema>> {
103
+ content: Static<ReturnType<typeof Type.Object<TSchemaShape>>>;
104
+ settings: Static<ReturnType<typeof Type.Object<TSettingsShape>>>;
105
+ }
106
+ /**
107
+ * The React component that renders the block. Should use the Field component returned by
108
+ * createBlock to render editable content.
109
+ */
110
+ type BlockToReact<TSchemaShape extends Record<string, TSchema>, TSettingsShape extends Record<string, TSchema>> = {
111
+ /** Renders the block, with its content and settings as props. */
112
+ toReact: React.ComponentType<BlockContext<TSchemaShape, TSettingsShape>>;
113
+ };
114
+ type CreateBlockOptions<TSchemaShape extends Record<string, TSchema> = Record<string, TSchema>, TSettingsShape extends Record<string, TSchema> = Record<string, TSchema>, TLayoutOnly extends boolean = false, TNodes extends BlockStructuredData = BlockStructuredData> = BlockBaseOptions<TSchemaShape, TSettingsShape, TLayoutOnly, TNodes> & BlockToReact<TSchemaShape, TSettingsShape>;
79
115
  interface BlockData<TContent> {
80
116
  _id: string;
81
117
  type: string;
@@ -99,6 +135,7 @@ export interface PeekItem {
99
135
  content: unknown;
100
136
  settings: Record<string, unknown> | null;
101
137
  summary: string;
138
+ title: string;
102
139
  position: string;
103
140
  createdAt: number;
104
141
  updatedAt: number;
@@ -115,7 +152,7 @@ export interface RepeatableItemSeed {
115
152
  settings?: Record<string, unknown>;
116
153
  position: string;
117
154
  }
118
- export declare function createEditableBlock<TSchemaShape extends Record<string, TSchema>, TSettingsShape extends Record<string, TSchema> = Record<string, never>, TLayoutOnly extends boolean = false>(options: CreateBlockOptions<TSchemaShape, TSettingsShape, TLayoutOnly>): {
155
+ export declare function createEditableBlock<TSchemaShape extends Record<string, TSchema>, TSettingsShape extends Record<string, TSchema> = Record<string, never>, TLayoutOnly extends boolean = false, TNodes extends BlockStructuredData = BlockStructuredData>(options: CreateBlockOptions<TSchemaShape, TSettingsShape, TLayoutOnly, TNodes>): {
119
156
  Detached: ({ children }: {
120
157
  children: (props: {
121
158
  "data-camox-block-id"?: string;
@@ -1657,17 +1694,19 @@ export declare function createEditableBlock<TSchemaShape extends Record<string,
1657
1694
  params: unknown[] & [];
1658
1695
  })["static"]; }, Exclude<keyof TSchemaShape, { [K_1 in keyof TSchemaShape]: TSchemaShape[K_1] extends import("@sinclair/typebox").TReadonly<TSchema> ? TSchemaShape[K_1] extends import("@sinclair/typebox").TOptional<TSchemaShape[K_1]> ? K_1 : never : never; }[keyof TSchemaShape] | { [K_2 in keyof TSchemaShape]: TSchemaShape[K_2] extends import("@sinclair/typebox").TReadonly<TSchema> ? TSchemaShape[K_2] extends import("@sinclair/typebox").TOptional<TSchemaShape[K_2]> ? never : K_2 : never; }[keyof TSchemaShape] | { [K_3 in keyof TSchemaShape]: TSchemaShape[K_3] extends import("@sinclair/typebox").TOptional<TSchema> ? TSchemaShape[K_3] extends import("@sinclair/typebox").TReadonly<TSchemaShape[K_3]> ? never : K_3 : never; }[keyof TSchemaShape]>>>>>) => React.JSX.Element;
1659
1696
  /** Renders a record page's record; see `BlockEditingRuntime.renderLayoutItem`. */
1660
- LayoutItem: ({ collectionId, labelField, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
1697
+ LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
1661
1698
  id: string;
1662
1699
  title: string;
1663
1700
  description: string;
1664
1701
  contentSchema: {
1702
+ toStructuredData?: unknown;
1665
1703
  type: "object";
1666
1704
  title: string;
1667
1705
  description: string;
1668
1706
  properties: TSchemaShape;
1669
1707
  required: string[];
1670
1708
  toMarkdown: string[];
1709
+ toTitle: string;
1671
1710
  };
1672
1711
  settingsSchema: {
1673
1712
  type: "object";
@@ -1705,6 +1744,7 @@ export declare function createEditableBlock<TSchemaShape extends Record<string,
1705
1744
  settings: Record<string, unknown>;
1706
1745
  slot: null;
1707
1746
  summary: string;
1747
+ title: string;
1708
1748
  position: string;
1709
1749
  createdAt: number;
1710
1750
  updatedAt: number;