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
@@ -5,18 +5,24 @@ import { UNNAMED_DEFAULT_LOCALE, localizedPath, parseLocalePath } from "../../li
5
5
  import { draftNamedBy } from "../../lib/draft-scope.js";
6
6
  import { RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute } from "./runtimeRoutes.js";
7
7
  import { reads } from "../../lib/studio-reads.js";
8
- import { asCollectionItem, recordMarkdownTemplate, recordPlainText } from "../../core/recordItem.js";
8
+ import { asCollectionItem, recordMarkdownTemplate, recordPageValues } from "../../core/recordItem.js";
9
+ import { buildLayoutContext, curatedLayoutContext } from "../../core/layoutContext.js";
9
10
  import { PLATFORM_SCRIPT } from "../../lib/platform.js";
10
11
  import { STUDIO_ASSETS_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, matchCollectionContentPath } from "../studio/routes.js";
11
12
  import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
13
+ import { DefaultPageImage, renderPageImage } from "../og/pageImage.js";
12
14
  import { discoverLayoutPages, matchDerivedLayout } from "../../core/derivedRoutes.js";
13
- import { buildCamoxPageHead, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter } from "../routes/pageRuntime.js";
15
+ import { buildCamoxPageHead, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, declarePage, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter } from "../routes/pageRuntime.js";
14
16
  import { alternatesByPage, findAlternates, hreflangLinks } from "./alternates.js";
15
17
  import { configuredProviders, renderAnalyticsScripts } from "./analytics.js";
16
18
  import { omitArtifactSources } from "./artifactSources.js";
19
+ import { readBreadcrumb } from "./breadcrumbs.js";
17
20
  import { RouteDiscoveryError, listLivePages } from "./livePages.js";
18
21
  import { formatLlmsTxt } from "./llmsTxt.js";
19
22
  import { getPageBlockTypes } from "./pageBlockTypes.js";
23
+ import { withPageUrls } from "./structuredData.js";
24
+ import { curatedPageDescription, pageRecords, placedBlockNodes, withStructuredData } from "./pageGraph.js";
25
+ import { createProjectLogoResponse, projectLogo } from "./projectLogo.js";
20
26
  import { DEVELOPMENT_DRAFT, LOCALE_HEADER } from "@camox/api-contract/drafts";
21
27
  import { QueryClient, dehydrate } from "@tanstack/react-query";
22
28
  import { createHead, renderSSRHead } from "@unhead/react/server";
@@ -151,15 +157,22 @@ function absoluteUrl(request, options) {
151
157
  const { origin } = new URL(request.url);
152
158
  return (path) => `${origin}${withRuntimeBasePath(path, options.runtimeBasePath)}`;
153
159
  }
160
+ /** An ETag derived from `text`. */
161
+ async function etagOf(text) {
162
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
163
+ return `"${[...new Uint8Array(digest)].slice(0, 16).map((byte) => byte.toString(16).padStart(2, "0")).join("")}"`;
164
+ }
165
+ /** Whether the request's `If-None-Match` names `etag`, so a 304 answers it. */
166
+ function namesEtag(request, etag) {
167
+ return (request.headers.get("If-None-Match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === etag || tag === "*");
168
+ }
154
169
  /** `body` with an ETag derived from it, or a 304 when the request's `If-None-Match` names it. */
155
170
  async function withEtag(request, body, headers) {
156
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
157
- const etag = `"${[...new Uint8Array(digest)].slice(0, 16).map((byte) => byte.toString(16).padStart(2, "0")).join("")}"`;
158
171
  const taggedHeaders = {
159
172
  ...headers,
160
- ETag: etag
173
+ ETag: await etagOf(body)
161
174
  };
162
- if ((request.headers.get("If-None-Match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === etag || tag === "*")) return new Response(null, {
175
+ if (namesEtag(request, taggedHeaders.ETag)) return new Response(null, {
163
176
  status: 304,
164
177
  headers: taggedHeaders
165
178
  });
@@ -250,21 +263,94 @@ async function createRoutesResponse(request, options) {
250
263
  });
251
264
  }
252
265
  }
266
+ /**
267
+ * The image of the page `/og?path=` names, by its path with its locale's prefix: its layout's
268
+ * `toImage`, or Camox's default image, rendered with the page's context. Only real pages have
269
+ * one, read as the page itself would be, in the draft the URL names for a member.
270
+ */
253
271
  async function createOgResponse(request, options) {
254
272
  const camoxApp = await options.getCamoxApp?.();
255
273
  if (!camoxApp) return Response.json({ message: "Camox app registry is not available." }, { status: 500 });
256
- const url = new URL(request.url);
257
- const layoutId = url.searchParams.get("layoutId") || "";
258
- const title = url.searchParams.get("title") || "";
259
- const description = url.searchParams.get("description") || "";
260
- const projectName = url.searchParams.get("projectName") || "";
261
- const layout = camoxApp.getLayoutById(layoutId);
262
- if (!layout?._internal.buildOgImage) return new Response("Not found", { status: 404 });
263
- return layout._internal.buildOgImage({
264
- title,
265
- description,
266
- projectName
274
+ const source = await readPageImageSource(request, options, camoxApp);
275
+ if (!source) return new Response("Not found", { status: 404 });
276
+ const headers = {
277
+ ETag: await etagOf(JSON.stringify([source.layout?.id ?? null, source.context])),
278
+ "Cache-Control": isPrivateRender(request, options) ? "private, no-store" : "public, no-cache"
279
+ };
280
+ if (namesEtag(request, headers.ETag)) return new Response(null, {
281
+ status: 304,
282
+ headers
283
+ });
284
+ const toImage = source.layout?.toImage ?? DefaultPageImage;
285
+ const image = await renderPageImage(toImage(source.context));
286
+ for (const [name, value] of Object.entries(headers)) image.headers.set(name, value);
287
+ return image;
288
+ }
289
+ /** The layout and context of the page `/og` names, or null when it names no page. */
290
+ async function readPageImageSource(request, options, app) {
291
+ const { apiUrl, projectUuid } = options;
292
+ const path = new URL(request.url).searchParams.get("path");
293
+ if (!apiUrl || !projectUuid || !path?.startsWith("/")) return null;
294
+ const located = await locateRequest(request, options, path);
295
+ if (matchLocalizedRoute(located.path)?.kind !== "page") return null;
296
+ const { project } = await resolveRequestMember(request, options);
297
+ const member = project !== void 0;
298
+ const authCookieHeader = member ? getServerAuthCookieHeader(request.headers) : void 0;
299
+ const requestedDraft = getRequestDraftId(request, options);
300
+ const api = (draftId) => createServerApiClient(apiUrl, {
301
+ authCookieHeader,
302
+ draftId,
303
+ locale: located.locale
267
304
  });
305
+ const match = matchDerivedLayout(app.getLayouts(), located.path);
306
+ /** A code-owned page: its loader runs in the draft the layout read settled on. */
307
+ const codeOwned = async (match) => {
308
+ const layout = match.layout._internal;
309
+ const { value: shared, draftId } = await readDraftOrLive(requestedDraft, member, (draftId) => api(draftId).layouts.get({
310
+ projectUuid,
311
+ layoutId: layout.id
312
+ }));
313
+ const result = await loadLayoutResult(app, match, createLayoutRequestContext(options, member ? "draft" : "live", {
314
+ authCookieHeader,
315
+ draftId,
316
+ locale: located.locale
317
+ }), located.locale);
318
+ const record = asCollectionItem(result);
319
+ const collection = record && app.getCollectionById(record.collectionId);
320
+ return {
321
+ layout,
322
+ context: buildLayoutContext(layout, {
323
+ path,
324
+ locale: located.locale.tag,
325
+ projectName: shared.projectName,
326
+ data: result?.data,
327
+ params: match.params,
328
+ record: record && collection ? recordPageValues(collection, record) : void 0
329
+ })
330
+ };
331
+ };
332
+ /** A curated page, or else a derived layout's page, as the page's HTML falls back. */
333
+ const curated = async () => {
334
+ try {
335
+ const { value: page } = await readDraftOrLive(requestedDraft, member, (draftId) => api(draftId).pages.getByPath({
336
+ path: located.path,
337
+ projectUuid
338
+ }));
339
+ return {
340
+ layout: page.layout ? app.getLayoutById(page.layout.layoutId)?._internal : void 0,
341
+ context: curatedLayoutContext(page, located.locale)
342
+ };
343
+ } catch (error) {
344
+ if (isNotFoundError(error) && match) return codeOwned(match);
345
+ throw error;
346
+ }
347
+ };
348
+ try {
349
+ return match?.layout._internal.kind === "singleton" ? await codeOwned(match) : await curated();
350
+ } catch (error) {
351
+ if (isNotFoundError(error)) return null;
352
+ throw error;
353
+ }
268
354
  }
269
355
  function createDefaultHeadInput(stylesheetUrl) {
270
356
  return {
@@ -303,9 +389,9 @@ function createPageHeadInput(head) {
303
389
  const pageLinks = [];
304
390
  for (const entry of links) {
305
391
  if (!entry.href) continue;
306
- if (entry.rel === "icon") {
392
+ if (entry.rel === "icon" || entry.rel === "canonical") {
307
393
  pageLinks.push({
308
- rel: "icon",
394
+ rel: entry.rel,
309
395
  href: entry.href,
310
396
  "data-camox-page-head": "true"
311
397
  });
@@ -319,10 +405,16 @@ function createPageHeadInput(head) {
319
405
  "data-camox-page-head": "true"
320
406
  });
321
407
  }
408
+ const pageScripts = (head.scripts ?? []).filter((entry) => entry.type === "application/ld+json").map((entry) => ({
409
+ type: "application/ld+json",
410
+ innerHTML: entry.text,
411
+ "data-camox-page-head": "true"
412
+ }));
322
413
  return {
323
414
  ...title && { title },
324
415
  meta: pageMeta,
325
- link: pageLinks
416
+ link: pageLinks,
417
+ script: pageScripts
326
418
  };
327
419
  }
328
420
  function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl, locale) {
@@ -400,16 +492,109 @@ function isPreviewHandoff(request, options) {
400
492
  * The alternates of the page a request renders, where it exists in each of the scope's locales,
401
493
  * and its head with their `hreflang` links. Without a head, there is none to add them to.
402
494
  */
403
- async function readAlternates(request, options, { located, identity, head, ...reading }) {
404
- const alternates = await findRequestAlternates(options, located, identity, reading);
405
- const links = hreflangLinks(alternates, located.locales, absoluteUrl(request, options));
495
+ async function readAlternates(request, options, { located, identity, head, described, ...reading }) {
496
+ const localeReads = requestLocaleReads(options, reading);
497
+ const [alternates, crumbs] = await Promise.all([findRequestAlternates(options, located, identity, localeReads), head ? readRequestBreadcrumb(options, located, described.title, localeReads) : []]);
498
+ const url = absoluteUrl(request, options);
499
+ const links = hreflangLinks(alternates, located.locales, url);
500
+ const place = {
501
+ url: url(localizedPath(located.path, located.locale)),
502
+ locale: located.locale.tag,
503
+ alternates
504
+ };
505
+ const site = {
506
+ url: url("/"),
507
+ name: described.project.name,
508
+ logo: projectLogo(url, described.project),
509
+ socialProfiles: described.project.socialProfiles ?? []
510
+ };
511
+ const breadcrumb = crumbs.map((crumb) => ({
512
+ name: crumb.name,
513
+ url: url(localizedPath(crumb.path, located.locale))
514
+ }));
515
+ const defaultLocale = located.locales.find((locale) => locale.isDefault) ?? located.locale;
516
+ const pageUrl = (marker) => {
517
+ const locale = located.locales.find((candidate) => candidate.tag === marker.locale);
518
+ return url(localizedPath(marker.path, locale ?? defaultLocale));
519
+ };
520
+ const blocks = withPageUrls(described.blocks ?? [], pageUrl);
521
+ const localeHome = url(localizedPath("/", located.locale));
522
+ const records = described.records && {
523
+ ...described.records,
524
+ nodes: withPageUrls(described.records.nodes, pageUrl),
525
+ id: (recordId) => `${localeHome}#record-${recordId}`
526
+ };
406
527
  return {
407
528
  alternates,
408
- head: head && withHreflangs(head, links)
529
+ head: head && withStructuredData(withLocatedTags(withHreflangs(head, links), place), place, site, {
530
+ ...described,
531
+ breadcrumb,
532
+ blocks,
533
+ records
534
+ })
535
+ };
536
+ }
537
+ /** An Open Graph locale: a locale tag with an underscore, as in `de_CH`. */
538
+ const ogLocale = (tag) => tag.replaceAll("-", "_");
539
+ /**
540
+ * A page's head with where it is: its canonical URL, which never names a draft, and its locale
541
+ * among the locales it exists in.
542
+ */
543
+ function withLocatedTags(head, { url, locale, alternates }) {
544
+ const others = alternates.filter((alternate) => alternate.locale !== locale);
545
+ return {
546
+ ...head,
547
+ meta: [
548
+ ...head.meta ?? [],
549
+ {
550
+ property: "og:url",
551
+ content: url
552
+ },
553
+ {
554
+ property: "og:locale",
555
+ content: ogLocale(locale)
556
+ },
557
+ ...others.map((other) => ({
558
+ property: "og:locale:alternate",
559
+ content: ogLocale(other.locale)
560
+ }))
561
+ ],
562
+ links: [...head.links ?? [], {
563
+ rel: "canonical",
564
+ href: url
565
+ }]
566
+ };
567
+ }
568
+ function requestLocaleReads(options, { authCookieHeader, draftId }) {
569
+ const { apiUrl, projectUuid } = options;
570
+ const reading = {
571
+ authCookieHeader: draftId ? authCookieHeader : void 0,
572
+ draftId
573
+ };
574
+ const source = draftId ? "draft" : "live";
575
+ const pagesByLocale = /* @__PURE__ */ new Map();
576
+ return {
577
+ pages: (locale) => {
578
+ if (!apiUrl || !projectUuid) return Promise.resolve([]);
579
+ const read = pagesByLocale.get(locale.tag) ?? createServerApiClient(apiUrl, {
580
+ ...reading,
581
+ locale
582
+ }).pages.listBySlug({ projectUuid }).then(({ pages }) => pages);
583
+ pagesByLocale.set(locale.tag, read);
584
+ return read;
585
+ },
586
+ context: (locale) => createLayoutRequestContext(options, source, {
587
+ ...reading,
588
+ locale
589
+ }),
590
+ records: (locale) => createRecordLookup(options, source, {
591
+ ...reading,
592
+ locale
593
+ })
409
594
  };
410
595
  }
411
596
  /** Where the page a request renders exists in each of the scope's locales: itself alone in one. */
412
- async function findRequestAlternates(options, located, identity, { authCookieHeader, draftId }) {
597
+ async function findRequestAlternates(options, located, identity, reads) {
413
598
  const { apiUrl, projectUuid } = options;
414
599
  const here = [{
415
600
  locale: located.locale.tag,
@@ -417,26 +602,33 @@ async function findRequestAlternates(options, located, identity, { authCookieHea
417
602
  }];
418
603
  if (!apiUrl || !projectUuid || located.locales.length < 2) return here;
419
604
  const app = await options.getCamoxApp?.();
420
- const reading = {
421
- authCookieHeader: draftId ? authCookieHeader : void 0,
422
- draftId
423
- };
424
605
  return findAlternates({
425
606
  identity,
426
607
  located,
427
608
  layouts: app?.getLayouts() ?? [],
428
- reads: {
429
- pages: async (locale) => (await createServerApiClient(apiUrl, {
430
- ...reading,
431
- locale
432
- }).pages.listBySlug({ projectUuid })).pages,
433
- context: (locale) => createLayoutRequestContext(options, draftId ? "draft" : "live", {
434
- ...reading,
435
- locale
436
- })
437
- }
609
+ reads
438
610
  });
439
611
  }
612
+ /**
613
+ * The crumbs of the page a request renders, in its locale. Without them, the page still renders:
614
+ * a breadcrumb that can't be read is left out.
615
+ */
616
+ async function readRequestBreadcrumb(options, located, title, reads) {
617
+ try {
618
+ return await readBreadcrumb({
619
+ path: located.path,
620
+ title,
621
+ app: await options.getCamoxApp?.(),
622
+ reads: {
623
+ pages: () => reads.pages(located.locale),
624
+ records: reads.records(located.locale)
625
+ }
626
+ });
627
+ } catch (error) {
628
+ console.error("[camox] Couldn't read the page's breadcrumb.", error);
629
+ return [];
630
+ }
631
+ }
440
632
  /** A page's head with its alternates' `hreflang` links. */
441
633
  function withHreflangs(head, hreflangs) {
442
634
  if (!hreflangs.length) return head;
@@ -515,6 +707,7 @@ async function createPageHtmlResponse({ options, pathname, located, request }) {
515
707
  pageId: result.data.page.page.id
516
708
  },
517
709
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : {},
710
+ described: curatedPageDescription(camoxApp, result.data.page, located.locale),
518
711
  authCookieHeader: getServerAuthCookieHeader(request.headers),
519
712
  draftId: result.draftId
520
713
  });
@@ -662,7 +855,7 @@ async function createLayoutMarkdownResponse(options, match, locale, page) {
662
855
  toMarkdown: template
663
856
  })],
664
857
  data,
665
- item: recordPlainText(collection, data.content)
858
+ record: recordPageValues(collection, record)
666
859
  };
667
860
  };
668
861
  let rendered;
@@ -685,13 +878,14 @@ async function createLayoutMarkdownResponse(options, match, locale, page) {
685
878
  });
686
879
  const meta = buildPageMeta({
687
880
  layout: match.layout._internal,
688
- pageMetaTitle: match.layout._internal.title,
689
- pageMetaDescription: void 0,
690
- data: rendered.data,
691
- item: rendered.item,
692
- params: match.params,
693
- projectName: slots.projectName,
694
- pageFullPath: page.pathname
881
+ context: buildLayoutContext(match.layout._internal, {
882
+ path: page.pathname,
883
+ locale: locale.tag,
884
+ projectName: slots.projectName,
885
+ data: rendered.data,
886
+ params: match.params,
887
+ record: "record" in rendered ? rendered.record : void 0
888
+ })
695
889
  });
696
890
  const markdown = [
697
891
  slots.before,
@@ -746,6 +940,15 @@ async function createDerivedResponse(options, pathname, located, request, { data
746
940
  }), serializedResult ?? null, shownIn);
747
941
  const record = asCollectionItem(serializedResult);
748
942
  const recordCollection = record && app.getCollectionById(record.collectionId);
943
+ const recordValues = record && recordCollection ? recordPageValues(recordCollection, record) : void 0;
944
+ const context = buildLayoutContext(match.layout._internal, {
945
+ path: pathname,
946
+ locale: located.locale.tag,
947
+ projectName: shared.projectName,
948
+ data: serializedResult?.data,
949
+ params: match.params,
950
+ record: recordValues
951
+ });
749
952
  const { alternates, head: pageHead } = await readAlternates(request, options, {
750
953
  located,
751
954
  identity: record && match.layout._internal.kind === "derived" ? {
@@ -755,17 +958,30 @@ async function createDerivedResponse(options, pathname, located, request, { data
755
958
  } : { kind: "code-owned" },
756
959
  head: buildPageHead({
757
960
  layout: match.layout._internal,
758
- pageMetaTitle: match.layout._internal.title,
759
- pageMetaDescription: void 0,
760
- data: serializedResult?.data,
761
- item: record && recordCollection ? recordPlainText(recordCollection, record.data.content) : void 0,
762
- params: match.params,
763
- projectName: shared.projectName,
764
- pageFullPath: pathname,
961
+ context,
765
962
  origin: new URL(request.url).origin,
766
963
  faviconUrl: buildFaviconUrl(options.apiUrl, shared.project),
767
- hasMarkdown: match.layout._internal.toMarkdown !== void 0
964
+ hasMarkdown: match.layout._internal.toMarkdown !== void 0,
965
+ recordType: record?.structuredData?.node?.["@type"]
768
966
  }),
967
+ described: {
968
+ project: {
969
+ ...shared.project,
970
+ name: shared.projectName
971
+ },
972
+ title: context.page.title,
973
+ declared: declarePage({
974
+ layout: match.layout._internal,
975
+ context
976
+ }),
977
+ blocks: placedBlockNodes(shared.layout, [], shared.structuredData),
978
+ ...record?.published && { published: record.published },
979
+ records: pageRecords(shared.recordStructuredData, record && {
980
+ id: record.data.id,
981
+ structuredData: record.structuredData,
982
+ titles: recordValues?.titles
983
+ })
984
+ },
769
985
  authCookieHeader,
770
986
  draftId: read.draftId
771
987
  });
@@ -794,6 +1010,11 @@ async function createDerivedResponse(options, pathname, located, request, { data
794
1010
  locales: located.locales,
795
1011
  derived: {
796
1012
  layoutId: match.layout._internal.id,
1013
+ page: {
1014
+ params: match.params,
1015
+ path: pathname,
1016
+ projectName: shared.projectName
1017
+ },
797
1018
  result: serializedResult,
798
1019
  layout: shared.layout
799
1020
  }
@@ -948,6 +1169,7 @@ async function createPageDataResponse({ options, pathname, located, request }) {
948
1169
  pageId: result.data.page.page.id
949
1170
  },
950
1171
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null,
1172
+ described: curatedPageDescription(camoxApp, result.data.page, located.locale),
951
1173
  authCookieHeader: getServerAuthCookieHeader(request.headers),
952
1174
  draftId: result.draftId
953
1175
  });
@@ -1000,6 +1222,7 @@ async function handleCamoxRequest(request, options = {}) {
1000
1222
  });
1001
1223
  }
1002
1224
  if (match.kind === "og") return createOgResponse(request, options);
1225
+ if (match.kind === "logo") return createProjectLogoResponse(request, options);
1003
1226
  if (match.kind === "sitemap") return createSitemapResponse(request, options);
1004
1227
  if (match.kind === "llms-txt") return createLlmsTxtResponse(request, options);
1005
1228
  if (match.kind === "routes") return createRoutesResponse(request, options);
@@ -11,6 +11,7 @@ export declare const RUNTIME_ROUTE_PATHS: {
11
11
  readonly health: "/_camox/health";
12
12
  readonly registry: "/_camox/registry";
13
13
  readonly routes: "/_camox/routes";
14
+ readonly logo: "/_camox/logo.png";
14
15
  readonly og: "/og";
15
16
  readonly sitemap: "/sitemap.xml";
16
17
  readonly "llms-txt": "/llms.txt";
@@ -11,6 +11,7 @@ const RUNTIME_ROUTE_PATHS = {
11
11
  health: "/_camox/health",
12
12
  registry: "/_camox/registry",
13
13
  routes: "/_camox/routes",
14
+ logo: "/_camox/logo.png",
14
15
  og: "/og",
15
16
  sitemap: "/sitemap.xml",
16
17
  "llms-txt": "/llms.txt"