camox 0.47.0 → 0.51.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/core/createApp.d.ts +19 -6
- package/dist/core/createApp.js +7 -2
- package/dist/core/createBlock.js +54 -43
- package/dist/core/createCollection.d.ts +25 -7
- package/dist/core/createCollection.js +28 -9
- package/dist/core/createLayout.d.ts +98 -71
- package/dist/core/createLayout.js +10 -16
- package/dist/core/derivedRoutes.js +1 -1
- package/dist/core/editing/BlockEditingRuntime.d.ts +3 -2
- package/dist/core/editing/CompleteBlockEditingRuntime.js +2 -1
- package/dist/core/editing/createEditableBlock.d.ts +59 -19
- package/dist/core/editing/createEditableBlock.js +162 -151
- package/dist/core/editing/useLatestRecord.js +7 -7
- package/dist/core/layoutContext.d.ts +55 -0
- package/dist/core/layoutContext.js +66 -0
- package/dist/core/lib/contentType.d.ts +91 -4
- package/dist/core/lib/contentType.js +84 -7
- package/dist/core/lib/recordScope.js +19 -17
- package/dist/core/lib/reference.d.ts +6 -2
- package/dist/core/recordItem.js +55 -2
- package/dist/core/requestContext.d.ts +18 -0
- package/dist/core/structuredData.d.ts +126 -0
- package/dist/core/structuredData.js +40 -0
- package/dist/features/canvas/canvasCamera.js +1 -1
- package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
- package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
- package/dist/features/content/ContentAssets.js +84 -88
- package/dist/features/content/ContentCollection.js +13 -7
- package/dist/features/content/collection-form.js +6 -2
- package/dist/features/content/components/MissingFromLocale.js +1 -1
- package/dist/features/og/pageImage.js +89 -0
- package/dist/features/page/CuratedPageContent.js +45 -32
- package/dist/features/page/DerivedPageContent.js +49 -22
- package/dist/features/page/SharedLayoutContent.js +26 -22
- package/dist/features/preview/CamoxPreview.js +41 -27
- package/dist/features/preview/EditablePageExperience.js +17 -9
- package/dist/features/preview/components/AddBlockSheet.js +1 -0
- package/dist/features/preview/components/BlockActionsPopover.js +19 -41
- package/dist/features/preview/components/FieldToolbar.js +1 -1
- package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
- package/dist/features/preview/components/PageInfo.js +244 -264
- package/dist/features/preview/components/PageTree.js +63 -53
- package/dist/features/preview/components/PreviewPanel.js +10 -64
- package/dist/features/preview/components/PreviewToolbar.js +1 -1
- package/dist/features/preview/components/RecordCard.js +50 -34
- package/dist/features/preview/components/RecordCombobox.js +12 -5
- package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
- package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
- package/dist/features/preview/components/RepeatableItemsList.js +62 -46
- package/dist/features/preview/components/SelectionEditor.js +3 -2
- package/dist/features/preview/components/useFieldWriter.js +2 -4
- package/dist/features/preview/components/useRecordView.js +2 -2
- package/dist/features/preview/entityName.js +11 -0
- package/dist/features/preview/pageRecord.js +17 -18
- package/dist/features/preview/previewConstants.js +2 -8
- package/dist/features/preview/usePreviewViewportActions.js +1 -1
- package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
- package/dist/features/provider/components/CommandPalette.js +2 -5
- package/dist/features/provider/signInShortcut.js +1 -2
- package/dist/features/provider/useAdminShortcuts.js +47 -53
- package/dist/features/routes/pageRuntime.d.ts +17 -2
- package/dist/features/routes/pageRuntime.js +61 -57
- package/dist/features/runtime/RuntimeChrome.js +1 -1
- package/dist/features/runtime/breadcrumbs.js +68 -0
- package/dist/features/runtime/livePages.js +3 -2
- package/dist/features/runtime/pageGraph.js +92 -0
- package/dist/features/runtime/projectLogo.js +110 -0
- package/dist/features/runtime/publicCache.js +83 -0
- package/dist/features/runtime/runtime.d.ts +24 -9
- package/dist/features/runtime/runtime.js +447 -130
- package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
- package/dist/features/runtime/runtimeRoutes.js +1 -0
- package/dist/features/runtime/structuredData.js +213 -0
- package/dist/features/runtime/worker.d.ts +5 -1
- package/dist/features/runtime/worker.js +20 -3
- package/dist/features/settings/AnalyticsSettings.js +622 -0
- package/dist/features/settings/CamoxSettings.js +95 -0
- package/dist/features/settings/FaviconSettings.js +254 -0
- package/dist/features/settings/ProjectNameSettings.js +145 -0
- package/dist/features/settings/SettingsPart.js +73 -0
- package/dist/features/settings/SocialProfilesSettings.js +245 -0
- package/dist/features/settings/useProjectMutation.js +61 -0
- package/dist/features/studio/components/ChatSidebar.js +5 -5
- package/dist/features/studio/components/DraftsControl.js +136 -131
- package/dist/features/studio/components/LocalePicker.js +178 -174
- package/dist/features/studio/components/Navbar.js +107 -66
- package/dist/features/studio/components/ProjectMenu.js +85 -110
- package/dist/features/studio/components/ReviewDraftSheet.js +1046 -990
- package/dist/features/studio/components/ThreadMessages.js +2 -2
- package/dist/features/studio/components/UserButton.js +1 -1
- package/dist/features/studio/routes.js +3 -1
- package/dist/features/studio/showInCanvas.js +129 -0
- package/dist/features/vite/blockBoilerplate.js +3 -2
- package/dist/features/vite/definitionsSync.js +4 -5
- package/dist/features/vite/layoutGeneration.js +3 -2
- package/dist/features/vite/runtimeDev.js +32 -17
- package/dist/features/vite/staticAssetHeaders.js +52 -0
- package/dist/features/vite/vite.d.ts +1 -1
- package/dist/features/vite/vite.js +17 -4
- package/dist/lib/api-client-server.d.ts +1 -1
- package/dist/lib/api-client.d.ts +2 -1
- package/dist/lib/draft-scope.d.ts +1 -0
- package/dist/lib/locale-path.d.ts +3 -11
- package/dist/lib/locale-path.js +3 -27
- package/dist/lib/queries.d.ts +4 -0
- package/dist/lib/queries.js +8 -1
- package/dist/lib/shortcuts.js +29 -0
- package/dist/lib/studio-reads.d.ts +1 -0
- package/dist/lib/studio-reads.js +3 -1
- package/dist/lib/utils.js +1 -104
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +6 -4
- package/skills/camox/references/block-definitions.md +105 -25
- package/skills/camox/references/layout-definitions.md +53 -17
- package/skills/camox/references/routed-layouts.md +17 -14
- package/dist/features/studio/components/FaviconDialog.js +0 -318
|
@@ -18,8 +18,9 @@ const myBlock = createBlock({
|
|
|
18
18
|
content: (field) => ({
|
|
19
19
|
title: field.string({ default: "Welcome" }),
|
|
20
20
|
}),
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
toReact: MyBlockComponent,
|
|
22
|
+
toTitle: ({ content }) => content.title,
|
|
23
|
+
toMarkdown: ({ content }) => [`# ${content.title}`],
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
function MyBlockComponent() {
|
|
@@ -37,17 +38,19 @@ For DOM listeners, scrolling, scripts, widgets, or JavaScript-driven animation,
|
|
|
37
38
|
|
|
38
39
|
## The `createBlock` options
|
|
39
40
|
|
|
40
|
-
| Option
|
|
41
|
-
|
|
|
42
|
-
| `id`
|
|
43
|
-
| `title`
|
|
44
|
-
| `description`
|
|
45
|
-
| `content`
|
|
46
|
-
| `settings`
|
|
47
|
-
| `layoutOnly`
|
|
48
|
-
| `synced`
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
41
|
+
| Option | Required | Description |
|
|
42
|
+
| ------------------ | -------- | ---------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `id` | yes | Unique kebab-case identifier matching the filename without extension. |
|
|
44
|
+
| `title` | yes | CMS display name. |
|
|
45
|
+
| `description` | yes | Agent guidance on placement, expected content, and tone. |
|
|
46
|
+
| `content` | yes | `(field) => ({ ... })`: field names mapped to `field.*` builders for editable content. |
|
|
47
|
+
| `settings` | no | `(setting) => ({ ... })`: settings-panel options; only `setting.enum` and `setting.boolean`. |
|
|
48
|
+
| `layoutOnly` | no | If true, restricts placement to layouts and hides the type from the add-block picker. |
|
|
49
|
+
| `synced` | no | Defaults to false. Shares content/settings across placements; see below. |
|
|
50
|
+
| `toReact` | yes | Named React function component. Receives `{ content, settings }` as props. |
|
|
51
|
+
| `toTitle` | yes | `({ content, settings }) => ...` one-line title; see below. |
|
|
52
|
+
| `toMarkdown` | yes | `({ content, settings }) => [...]` markdown builder; see below. |
|
|
53
|
+
| `toStructuredData` | no | `({ content, settings }) => node \| [...]`: the things the block shows, as JSON-LD; see below. |
|
|
51
54
|
|
|
52
55
|
## Synced blocks (`synced`, optional)
|
|
53
56
|
|
|
@@ -59,14 +62,30 @@ For shared navbar/footer data across layouts, use `layoutOnly: true, synced: tru
|
|
|
59
62
|
- Position, layout slot, and deletion remain per placement; deleting one placement leaves the others.
|
|
60
63
|
- Enabling syncing on an existing type applies the oldest placement's data to all placements. Disabling it leaves independently editable copies.
|
|
61
64
|
|
|
65
|
+
## Titles (`toTitle`)
|
|
66
|
+
|
|
67
|
+
Required on blocks and repeaters. Names each block and repeatable item in the studio and to agents until it has a Summary, which always wins once it exists. Receives `{ content, settings }` like `toMarkdown` and returns one line: a field, a template literal, or a fixed string for blocks with no telling field.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
toTitle: ({ content }) => content.title;
|
|
71
|
+
toTitle: ({ content }) => `${content.number} ${content.label}`;
|
|
72
|
+
toTitle: () => "Navigation";
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Titles read the default locale's content as plain text: a Link reads as its text. References aren't resolved in titles, so name a block by its own fields.
|
|
76
|
+
|
|
62
77
|
## Markdown Representation (`toMarkdown`)
|
|
63
78
|
|
|
64
|
-
Required on blocks and repeaters. Receives typed proxies
|
|
79
|
+
Required on blocks and repeaters. Receives `{ content, settings }`, typed proxies of the block's content and settings; returns entries joined with `\n\n` for AI summaries and SEO. Lines whose referenced fields are all empty, or whose condition is false, are omitted.
|
|
65
80
|
|
|
66
|
-
Use bare `
|
|
81
|
+
Use bare `content.field` for single-field lines and template literals for markdown or combined fields:
|
|
67
82
|
|
|
68
83
|
```tsx
|
|
69
|
-
toMarkdown: (
|
|
84
|
+
toMarkdown: ({ content }) => [
|
|
85
|
+
`> ${content.quote}`,
|
|
86
|
+
`— ${content.author}, ${content.company}`,
|
|
87
|
+
content.photo,
|
|
88
|
+
];
|
|
70
89
|
```
|
|
71
90
|
|
|
72
91
|
Field resolution:
|
|
@@ -87,16 +106,16 @@ Field resolution:
|
|
|
87
106
|
Boolean settings wrap one line or an array; Enum settings match a variant:
|
|
88
107
|
|
|
89
108
|
```tsx
|
|
90
|
-
toMarkdown: (
|
|
91
|
-
`# ${
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
toMarkdown: ({ content, settings }) => [
|
|
110
|
+
`# ${content.title}`,
|
|
111
|
+
settings.showCta(content.cta),
|
|
112
|
+
settings.showDetails([content.subtitle, content.description]),
|
|
113
|
+
settings.variant("banner", `**${content.headline}** — ${content.subtext}`),
|
|
114
|
+
settings.variant("inline", `${content.headline}: ${content.cta}`),
|
|
96
115
|
];
|
|
97
116
|
```
|
|
98
117
|
|
|
99
|
-
Only Boolean/Enum settings support these conditionals. A repeater's `
|
|
118
|
+
Only Boolean/Enum settings support these conditionals. A repeater's `settings` refers to the item's settings, not the parent block's.
|
|
100
119
|
|
|
101
120
|
## Content Field Types
|
|
102
121
|
|
|
@@ -203,7 +222,7 @@ field.artifact({ size: "fill", data: ["headline", "variant"] }); // shares sibli
|
|
|
203
222
|
|
|
204
223
|
### field.repeater
|
|
205
224
|
|
|
206
|
-
Structured items with their own content, optional settings, and required `toMarkdown`. `minItems` must be at least 1; repeaters cannot be empty. Nest repeaters in an item's `content` when needed.
|
|
225
|
+
Structured items with their own content, optional settings, and required `toTitle` and `toMarkdown`. `minItems` must be at least 1; repeaters cannot be empty. Nest repeaters in an item's `content` when needed.
|
|
207
226
|
|
|
208
227
|
```tsx
|
|
209
228
|
field.repeater({
|
|
@@ -217,12 +236,73 @@ field.repeater({
|
|
|
217
236
|
minItems: 1,
|
|
218
237
|
maxItems: 10,
|
|
219
238
|
title: "Features",
|
|
220
|
-
|
|
239
|
+
toTitle: ({ content }) => content.name,
|
|
240
|
+
toMarkdown: ({ content, settings }) => [
|
|
241
|
+
`### ${content.name}`,
|
|
242
|
+
settings.highlighted(content.description),
|
|
243
|
+
],
|
|
221
244
|
});
|
|
222
245
|
```
|
|
223
246
|
|
|
224
247
|
Reference the repeater field in the parent block's `toMarkdown` to include its rendered items.
|
|
225
248
|
|
|
249
|
+
## Structured Data (`toStructuredData`, optional)
|
|
250
|
+
|
|
251
|
+
On blocks and repeaters. Describes the things a block shows (a video, a product, questions, reviews) as schema.org nodes in the page's JSON-LD graph. Returns one node or a list. Camox links each node to the page (`isPartOf` for creative works, `subjectOf` for anything else) and gives each placement its own nodes. Only the layout says what the page is, so a block never returns a kind of web page, and Camox alone emits the site's `Organization` and `WebSite`. `@type` and properties are typed: invented types or properties, or a node missing what search engines require (a `Product`'s offers, review or rating), fail to compile.
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
toStructuredData: ({ content }) => ({
|
|
255
|
+
"@type": "VideoObject",
|
|
256
|
+
name: content.title,
|
|
257
|
+
embedUrl: content.video,
|
|
258
|
+
thumbnailUrl: content.thumbnail,
|
|
259
|
+
uploadDate: "2026-01-15",
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Field tokens resolve as follows; template literals read their fields as plain text:
|
|
264
|
+
|
|
265
|
+
| Type | Value |
|
|
266
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
267
|
+
| String | Plain text |
|
|
268
|
+
| Image | An `ImageObject` with its absolute URL, width, height and alt text |
|
|
269
|
+
| File | Its absolute URL |
|
|
270
|
+
| Link | The page's absolute URL in the page's locale, or the external href |
|
|
271
|
+
| Embed | Its URL |
|
|
272
|
+
| Repeater | Its items' nodes, from the repeater's own `toStructuredData` |
|
|
273
|
+
| Reference | A link to the record's node, from its collection's `toStructuredData`, or the record's title when the collection has none |
|
|
274
|
+
| Reference list | A list of such links |
|
|
275
|
+
|
|
276
|
+
Icon and artifact fields have no token. A property whose field is empty is left out. A reference's fields are tokens too (`content.author.name`). The place referencing a record decides its role, such as a testimonial's `author: content.author`; Camox adds the record's node to the graph once, however many blocks link it.
|
|
277
|
+
|
|
278
|
+
Place a repeater's token where its items' nodes go, such as an FAQ's questions:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
items: field.repeater({
|
|
282
|
+
content: (field) => ({ question: field.string({ default: "Why?" }), answer: field.string({ default: "Because" }) }),
|
|
283
|
+
minItems: 1,
|
|
284
|
+
maxItems: 20,
|
|
285
|
+
toTitle: ({ content }) => content.question,
|
|
286
|
+
toMarkdown: ({ content }) => [`Q: ${content.question}`, `A: ${content.answer}`],
|
|
287
|
+
toStructuredData: ({ content }) => ({
|
|
288
|
+
"@type": "Question",
|
|
289
|
+
name: content.question,
|
|
290
|
+
acceptedAnswer: { "@type": "Answer", text: content.answer },
|
|
291
|
+
}),
|
|
292
|
+
}),
|
|
293
|
+
// On the block:
|
|
294
|
+
toStructuredData: ({ content }) => content.items,
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Settings show a part of the template only while they hold: a Boolean setting takes the part, an Enum setting the option it must equal, then the part. The part is left out otherwise, and a list it shows inside a list adds its entries:
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
toStructuredData: ({ content, settings }) => [
|
|
301
|
+
{ "@type": "Product", name: content.name, description: settings.variant("detailed", content.description), offers: { "@type": "Offer", price: 49, priceCurrency: "EUR" } },
|
|
302
|
+
settings.showFaq(content.questions),
|
|
303
|
+
],
|
|
304
|
+
```
|
|
305
|
+
|
|
226
306
|
## Rendering in the Component
|
|
227
307
|
|
|
228
308
|
Field renderers receive `(props, data)`:
|
|
@@ -25,8 +25,9 @@ const myLayout = createLayout({
|
|
|
25
25
|
after: [footerBlock],
|
|
26
26
|
initial: [() => import("../blocks/hero")],
|
|
27
27
|
},
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
toReact: MyLayoutComponent,
|
|
29
|
+
toTitle: ({ page, project }) => `${page.title} | ${project.name}`,
|
|
30
|
+
toDescription: ({ page }) => page.description,
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
function MyLayoutComponent({ children }: { children: React.ReactNode }) {
|
|
@@ -44,16 +45,28 @@ export { myLayout as layout };
|
|
|
44
45
|
|
|
45
46
|
## The `createLayout` options
|
|
46
47
|
|
|
47
|
-
| Option
|
|
48
|
-
|
|
|
49
|
-
| `id`
|
|
50
|
-
| `title`
|
|
51
|
-
| `description`
|
|
52
|
-
| `blocks`
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
48
|
+
| Option | Required | Description |
|
|
49
|
+
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------ |
|
|
50
|
+
| `id` | yes | Unique kebab-case identifier matching the filename without extension. |
|
|
51
|
+
| `title` | yes | CMS display name. |
|
|
52
|
+
| `description` | yes | Guidance for users and agents on which pages suit this layout. |
|
|
53
|
+
| `blocks` | yes | `before`, `after`, and optional `initial` arrays; see layout slot rules below. |
|
|
54
|
+
| `toReact` | yes | Named function component receiving the context and `{ children }` as props. |
|
|
55
|
+
| `toTitle` | yes | `(context) => string`: the head title, `og:title`, and the Markdown front matter's `title`. |
|
|
56
|
+
| `toDescription` | yes | `(context) => string \| undefined`: the head description and `og:description`. May return `undefined`. |
|
|
57
|
+
| `toImage` | no | `(context) => JSX`: the sharing image; see below. |
|
|
58
|
+
| `toStructuredData` | no | `(context) => node \| undefined`: what the page is, in its JSON-LD; see below. |
|
|
59
|
+
|
|
60
|
+
## The `to*` options and their context
|
|
61
|
+
|
|
62
|
+
Every `to*` option receives the same context object, and none receives another one's output: Camox joins the outputs itself.
|
|
63
|
+
|
|
64
|
+
- `page.title`: the page's undecorated title. On curated pages, the editor's or generated meta title.
|
|
65
|
+
- `page.description`: on curated pages, the editor's or generated description. Absent otherwise.
|
|
66
|
+
- `page.path`: the page's path, with its locale's prefix. `page.locale`: the locale's tag, such as `de`.
|
|
67
|
+
- `project.name`: the project's name.
|
|
68
|
+
|
|
69
|
+
Singleton and derived layouts also receive `data` and `params`, and record pages `content`; see [Routed layouts](routed-layouts.md#metadata-and-markdown).
|
|
57
70
|
|
|
58
71
|
## Layout Slots — `before`, `after`, and `initial`
|
|
59
72
|
|
|
@@ -70,12 +83,12 @@ export { myLayout as layout };
|
|
|
70
83
|
- At most one layout per app may define `initial`; multiple definitions cause a startup error.
|
|
71
84
|
- Without `initial`, Camox seeds an empty homepage using the first curated layout. Apps with only singleton/derived layouts do not seed a curated homepage.
|
|
72
85
|
|
|
73
|
-
## OG Image — `
|
|
86
|
+
## OG Image — `toImage` (optional)
|
|
74
87
|
|
|
75
88
|
Returns JSX rendered as a 1200×630 image. Use **inline styles, not Tailwind**, and `display: "flex"` for layout: this runs in an image-generation engine, not a browser.
|
|
76
89
|
|
|
77
90
|
```tsx
|
|
78
|
-
|
|
91
|
+
toImage: ({ page, project }) => (
|
|
79
92
|
<div
|
|
80
93
|
style={{
|
|
81
94
|
display: "flex",
|
|
@@ -88,9 +101,32 @@ buildOgImage: ({ title, description, projectName }) => (
|
|
|
88
101
|
fontFamily: "sans-serif",
|
|
89
102
|
}}
|
|
90
103
|
>
|
|
91
|
-
{
|
|
92
|
-
<div style={{ fontSize: 64 }}>{title}</div>
|
|
93
|
-
{description && <div style={{ fontSize: 28 }}>{description}</div>}
|
|
104
|
+
{project.name && <div style={{ fontSize: 24 }}>{project.name}</div>}
|
|
105
|
+
<div style={{ fontSize: 64 }}>{page.title}</div>
|
|
106
|
+
{page.description && <div style={{ fontSize: 28 }}>{page.description}</div>}
|
|
94
107
|
</div>
|
|
95
108
|
),
|
|
96
109
|
```
|
|
110
|
+
|
|
111
|
+
The site serves the image at `/og?path=<page path>` for real pages only, rendered from the page as it is published. An editor's custom image replaces it, and a layout without one gets Camox's default image showing the page title and project name.
|
|
112
|
+
|
|
113
|
+
## Structured Data — `toStructuredData` (optional)
|
|
114
|
+
|
|
115
|
+
Every page's head carries a JSON-LD graph that Camox builds itself: the project's `Organization` and `WebSite`, and a `WebPage` named and described by `toTitle` and `toDescription`, with its URL, language, image and publish dates. `toStructuredData` only says what the page is. Return one of:
|
|
116
|
+
|
|
117
|
+
- **A kind of web page**, which the page's node becomes: `AboutPage`, `ContactPage`, `CollectionPage`, `FAQPage`, `ProfilePage`, `ItemPage`, `QAPage`, `SearchResultsPage`, `CheckoutPage`, `MedicalWebPage` or `RealEstateListing`.
|
|
118
|
+
- **A main entity**, emitted as its own node and linked as the page's `mainEntity`: an Article type (`BlogPosting`, `NewsArticle`, `TechArticle`, …), `Product`, `Event`, `Person`, `Place`, `Review`, `Question`, `HowTo`, `Recipe`, `Course`, `JobPosting`, `SoftwareApplication`, `VideoObject`, `ImageObject`, `Book`, `Service`, …. Camox fills what you leave out: the name (an Article's `headline`), description, URL and image, plus the dates, publisher and language of creative works. Values you set win. An Article makes `og:type` `article`.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
toStructuredData: () => ({ "@type": "AboutPage" }),
|
|
122
|
+
// A derived layout: the post is the page's main entity.
|
|
123
|
+
toStructuredData: ({ data }) => ({ "@type": "BlogPosting", wordCount: data.wordCount }),
|
|
124
|
+
toStructuredData: ({ data }) => ({
|
|
125
|
+
"@type": "Product",
|
|
126
|
+
offers: { price: data.price, priceCurrency: "EUR" },
|
|
127
|
+
}),
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- Only the listed types type-check, each with only its schema.org properties. There's no way out to the rest of schema.org.
|
|
131
|
+
- Properties search engines require and Camox can't fill are required, such as a `Product`'s `offers`, `review` or `aggregateRating`, an `Event`'s `startDate` and `location`, and a `JobPosting`'s `datePosted` and `hiringOrganization`.
|
|
132
|
+
- Layouts can't return `Organization` or `WebSite`: Camox builds the website's own from the project.
|
|
@@ -29,10 +29,11 @@ export const layout = createLayout("pokedex")({
|
|
|
29
29
|
after: [footerBlock],
|
|
30
30
|
},
|
|
31
31
|
loader: async () => ({ kind: "data", data: await loadPokemon() }),
|
|
32
|
-
// After the loader: these
|
|
33
|
-
|
|
32
|
+
// After the loader: these options' `data` is inferred from it.
|
|
33
|
+
toTitle: ({ page }) => page.title,
|
|
34
|
+
toDescription: ({ data }) => `${data.length} Pokémon to browse.`,
|
|
34
35
|
toMarkdown: ({ data }) => [`${data.length} Pokémon`, data.map((p) => `- ${p.name}`).join("\n")],
|
|
35
|
-
|
|
36
|
+
toReact: PokedexPage,
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
function PokedexPage() {
|
|
@@ -105,10 +106,10 @@ export const Layout = createLayout("articles.$slug")({
|
|
|
105
106
|
description: "One article, found by its slug.",
|
|
106
107
|
blocks: { before: [navbarBlock], after: [footerBlock] },
|
|
107
108
|
...articles.pages({ by: "slug", param: "slug" }),
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
toTitle: ({ page, project }) => `${page.title} | ${project.name}`,
|
|
110
|
+
toDescription: ({ content }) => content.excerpt || undefined,
|
|
110
111
|
toMarkdown: ({ item }) => [`# ${item.title}`, `By ${item.author.name}`, item.body],
|
|
111
|
-
|
|
112
|
+
toReact: ArticlePage,
|
|
112
113
|
});
|
|
113
114
|
|
|
114
115
|
function ArticlePage() {
|
|
@@ -133,11 +134,13 @@ function ArticlePage() {
|
|
|
133
134
|
- Several records sharing a value in the same source is a server error naming the collection, field, value and records. Camox does not enforce uniqueness: keep the field unique.
|
|
134
135
|
- The loader returns a `collection-item` envelope, `{ kind: "collection-item", collectionId, data: { id, content } }`, typed as `LayoutCollectionItemResult` from `camox/createLayout`. `layout.useData()` returns the unwrapped record, `{ id, content }`. Camox rejects an envelope naming an unregistered collection or a malformed record id.
|
|
135
136
|
- `routes` lists one entry per record that can have a page, in the request's source: the sitemap lists published records only, even when an editor asks, while the Canvas previews a signed-in editor's first record, draft-only records included. Each entry is `{ params, target: { kind: "collection-item", collectionId, id } }`; `target` names the record independently of its current value. Records whose value is empty or contains `/` are left out. Several records sharing a value fail discovery, so `/sitemap.xml` responds 500 naming the layout and the records. An empty collection returns `{ data: [] }`, and the Canvas asks for a path.
|
|
136
|
-
- `layout.Item` renders the page's record with the same typed scope as a block's `Reference`: `Field`, `Image`, `File`, `Embed`, `ImageList`, `FileList`, `Reference` and `ReferenceList`, plus `id` and `
|
|
137
|
+
- `layout.Item` renders the page's record with the same typed scope as a block's `Reference`: `Field`, `Image`, `File`, `Embed`, `ImageList`, `FileList`, `Reference` and `ReferenceList`, plus `id`, `title` and `description` (from the collection's `toTitle` and `toDescription`). Field names are checked against the collection. The record is the first hop: `Reference` and `ReferenceList` render the records it links, which link nothing further. Prefer it to `useData()`, whose `content` holds stored values (String fields may be rich-text state).
|
|
137
138
|
- `layout.Item` exists only when every return of the loader is a record of one collection. On a loader that may return external data or records of several collections, using it is a type error. At runtime it renders nothing when the result isn't a record.
|
|
138
139
|
- In Studio, editors edit record page fields in place, like a referenced record's: purple overlays, selection, and the record's fields in the sidebar. Edits change the shared record everywhere it appears.
|
|
139
140
|
- `toMarkdown` also receives `item`, the same Markdown builder as a reference list's `toMarkdown`: interpolate fields (`item.title`, `item.author.name`, `item.cover`) and Camox fills them with the live record, rich text included. `data` stays the raw record. Markdown always shows live content, whoever asks.
|
|
140
|
-
- `
|
|
141
|
+
- `toTitle`, `toDescription`, `toImage`, `toReact` and `toStructuredData` also receive `content`, the record's String fields as plain text and its references as links to their records' Structured Data nodes. Their `page.title` is the record's title.
|
|
142
|
+
- A collection's `toStructuredData` describes each of its records as one node, from tokens of its fields: `toStructuredData: ({ content }) => ({ "@type": "BlogPosting", author: content.author, image: content.cover })`. Camox fills its name (an Article's `headline`) and description from the record's title and description, and gives it one `@id` per record and locale, which every page referencing the record links. Collections may describe records as an `Organization` (a customer, a partner), never as the `WebSite`.
|
|
143
|
+
- When the layout's `toStructuredData` declares nothing, the record's node is the page's main entity, dated by the record's publish times. Otherwise link the record's references from the layout, such as `toStructuredData: ({ content }) => ({ "@type": "BlogPosting", author: content.author })`. A reference to a collection without `toStructuredData` reads as the record's title. Records the page's content references that no node links are listed as the page's `mentions`.
|
|
141
144
|
- To wrap the helper, pass the whole loader input through, `context` included, and return the envelope unchanged. Type it as the helper's loader (`ReturnType<typeof articlePages.loader>`) to keep `layout.Item` and `item` typed:
|
|
142
145
|
|
|
143
146
|
```tsx
|
|
@@ -162,22 +165,22 @@ The playground's `/pokedex`, `/pokemon/pikachu`, and `/pokemon-types/electric` a
|
|
|
162
165
|
|
|
163
166
|
## Metadata and Markdown
|
|
164
167
|
|
|
165
|
-
Singleton and derived pages get the same head as curated pages: title, description, `og:*` tags, the `/og` image, and the favicon. They have no editor metadata, so the
|
|
168
|
+
Singleton and derived pages get the same head as curated pages: title, description, `og:*` tags, the `/og` image, and the favicon. They have no editor metadata, so the `to*` options also receive the loader's unwrapped `data` and the route `params`:
|
|
166
169
|
|
|
167
170
|
```tsx
|
|
168
171
|
export const Layout = createLayout("pokemon.$name")({
|
|
169
172
|
kind: "derived",
|
|
170
173
|
// Other options omitted.
|
|
171
174
|
loader: async ({ params }) => ({ kind: "data", data: await loadPokemon(params.name) }),
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
toTitle: ({ data, page }) => `${data.name} – ${page.title}`,
|
|
176
|
+
toDescription: ({ data }) => `${data.name} is a ${data.types.join("/")} type Pokémon.`,
|
|
174
177
|
toMarkdown: ({ data, params }) => [`# ${data.name}`, `Types: ${data.types.join(", ")}`],
|
|
175
178
|
});
|
|
176
179
|
```
|
|
177
180
|
|
|
178
|
-
- **Put `loader` before `
|
|
179
|
-
- `
|
|
180
|
-
- `
|
|
181
|
+
- **Put `loader` before `toTitle`, `toDescription`, `toImage`, `toReact`, and `toMarkdown`.** TypeScript infers their `data` from the loader only in that order. Otherwise the loader is inferred as `never` and fails to type-check.
|
|
182
|
+
- `toTitle`, `toDescription`, `toImage` and `toReact` receive the same context as on curated pages (see [Layout Definitions](layout-definitions.md#the-to-options-and-their-context)), plus `data` and `params`. Here `page.title` is the layout's `title` (the record's title on record pages) and `page.description` is absent.
|
|
183
|
+
- `toDescription` may return `undefined`: these pages then have no description.
|
|
181
184
|
- `toMarkdown` returns the page's Markdown Representation as a list of strings. Empty strings are dropped and the rest joined with blank lines. A request with `Accept: text/markdown` gets YAML front matter (the `title` and `description` the head shows, and the page's `url`), then the live before-block Markdown, then this output, then the live after-block Markdown. Curated pages' Markdown opens with the same front matter. Without `toMarkdown`, the page answers HTML only. `throw notFound()` in the loader answers 404. Curated layouts cannot declare `toMarkdown`: their Markdown comes from their blocks.
|
|
182
185
|
- The playground's Pokémon, Pokémon type, and Pokédex layouts declare these. Try `curl -H "Accept: text/markdown" http://localhost:3000/pokemon/pikachu`.
|
|
183
186
|
|