camox 0.48.0 → 0.52.1

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 (127) hide show
  1. package/dist/core/createApp.d.ts +1 -0
  2. package/dist/core/createApp.js +1 -0
  3. package/dist/core/editing/contentEdits.js +78 -0
  4. package/dist/core/editing/createEditableBlock.js +277 -311
  5. package/dist/core/layoutContext.d.ts +1 -0
  6. package/dist/core/lib/contentType.js +2 -2
  7. package/dist/core/recordItem.js +3 -3
  8. package/dist/features/canvas/CamoxCanvas.js +15 -15
  9. package/dist/features/canvas/CanvasLoadingScreen.js +15 -11
  10. package/dist/features/canvas/CanvasPageFrame.js +81 -87
  11. package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
  12. package/dist/features/canvas/DerivedPagePathInput.js +12 -12
  13. package/dist/features/canvas/canvasOverlayGeometry.js +1 -1
  14. package/dist/features/canvas/canvasStore.js +2 -2
  15. package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
  16. package/dist/features/canvas/sidebar/CuratedPageInspector.js +1 -1
  17. package/dist/features/canvas/sidebar/LayoutPageBlocks.js +23 -31
  18. package/dist/features/canvas/useCanvasCamera.js +1 -1
  19. package/dist/features/canvas/useDiscoveredPreviewPaths.js +16 -9
  20. package/dist/features/content/ContentAssets.js +84 -88
  21. package/dist/features/page/CuratedPageContent.js +1 -1
  22. package/dist/features/preview/CamoxPreview.d.ts +8 -4
  23. package/dist/features/preview/CamoxPreview.js +258 -443
  24. package/dist/features/preview/EditablePageContent.js +244 -2
  25. package/dist/features/preview/EditablePageExperience.js +19 -13
  26. package/dist/features/preview/components/AddBlockSheet.js +55 -15
  27. package/dist/features/preview/components/BlockActionsPopover.js +23 -51
  28. package/dist/features/preview/components/BlockPeek.js +66 -0
  29. package/dist/features/preview/components/BlockThumbnail.js +134 -119
  30. package/dist/features/preview/components/CanvasPreview.js +1 -1
  31. package/dist/features/preview/components/CreatePageModal.js +150 -156
  32. package/dist/features/preview/components/FieldToolbar.js +1 -1
  33. package/dist/features/preview/components/Frame.js +27 -4
  34. package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
  35. package/dist/features/preview/components/PageInfo.js +161 -126
  36. package/dist/features/preview/components/PagePicker.js +2 -2
  37. package/dist/features/preview/components/PageTree.js +174 -190
  38. package/dist/features/preview/components/PreviewPanel.js +13 -67
  39. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  40. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -4
  41. package/dist/features/preview/components/ReferenceListFieldEditor.js +4 -4
  42. package/dist/features/preview/components/RemovedBlockPlaceholder.js +37 -70
  43. package/dist/features/preview/components/RepeatableItemsList.js +1 -1
  44. package/dist/features/preview/components/SelectionEditor.js +248 -256
  45. package/dist/features/preview/components/StudioEditingFallback.js +22 -26
  46. package/dist/features/preview/components/useFieldWriter.js +19 -19
  47. package/dist/features/preview/components/useRecordView.js +105 -147
  48. package/dist/features/preview/components/useRepeatableItemActions.js +0 -2
  49. package/dist/features/preview/pageRecord.js +30 -24
  50. package/dist/features/preview/previewConstants.js +2 -8
  51. package/dist/features/preview/previewStore.js +2 -0
  52. package/dist/features/preview/studioNotice.js +20 -0
  53. package/dist/features/preview/usePreviewViewportActions.js +1 -1
  54. package/dist/features/provider/AuthenticatedCamoxProvider.js +47 -31
  55. package/dist/features/provider/components/CommandPalette.js +2 -5
  56. package/dist/features/provider/signInShortcut.js +1 -2
  57. package/dist/features/provider/useAdminShortcuts.js +47 -53
  58. package/dist/features/renderer/RendererApp.js +542 -0
  59. package/dist/features/renderer/RendererFrames.js +450 -0
  60. package/dist/features/renderer/definitionAnswers.js +26 -0
  61. package/dist/features/renderer/protocol.js +35 -0
  62. package/dist/features/renderer/rendererHost.js +249 -0
  63. package/dist/features/renderer/studioConnection.js +186 -0
  64. package/dist/features/routes/pageRuntime.d.ts +2 -1
  65. package/dist/features/routes/pageRuntime.js +34 -22
  66. package/dist/features/runtime/RuntimeChrome.js +2 -5
  67. package/dist/features/runtime/assetPrefix.js +14 -0
  68. package/dist/features/runtime/breadcrumbs.js +1 -1
  69. package/dist/features/runtime/editingFallback.js +4 -8
  70. package/dist/features/runtime/livePages.js +1 -1
  71. package/dist/features/runtime/pageApp.js +57 -83
  72. package/dist/features/runtime/pageClient.js +1 -1
  73. package/dist/features/runtime/pageNavigation.js +3 -2
  74. package/dist/features/runtime/publicCache.js +95 -0
  75. package/dist/features/runtime/publishingDraft.js +23 -0
  76. package/dist/features/runtime/rendererClient.d.ts +8 -0
  77. package/dist/features/runtime/rendererClient.js +77 -0
  78. package/dist/features/runtime/runtime.d.ts +22 -3
  79. package/dist/features/runtime/runtime.js +209 -96
  80. package/dist/features/runtime/runtimeRoutes.d.ts +2 -0
  81. package/dist/features/runtime/runtimeRoutes.js +2 -0
  82. package/dist/features/runtime/worker.d.ts +14 -1
  83. package/dist/features/runtime/worker.js +34 -4
  84. package/dist/features/settings/AnalyticsSettings.js +622 -0
  85. package/dist/features/settings/CamoxSettings.js +95 -0
  86. package/dist/features/settings/FaviconSettings.js +254 -0
  87. package/dist/features/settings/ProjectNameSettings.js +145 -0
  88. package/dist/features/settings/SettingsPart.js +73 -0
  89. package/dist/features/settings/SocialProfilesSettings.js +245 -0
  90. package/dist/features/settings/useProjectMutation.js +61 -0
  91. package/dist/features/studio/FollowAgentProvider.js +10 -10
  92. package/dist/features/studio/components/DraftsControl.js +136 -131
  93. package/dist/features/studio/components/LocalePicker.js +178 -174
  94. package/dist/features/studio/components/Navbar.js +107 -66
  95. package/dist/features/studio/components/ProjectMenu.js +119 -171
  96. package/dist/features/studio/components/ReviewDraftSheet.js +1151 -991
  97. package/dist/features/studio/components/ThreadMessages.js +100 -65
  98. package/dist/features/studio/components/UserButton.js +2 -2
  99. package/dist/features/studio/routes.js +3 -1
  100. package/dist/features/studio/showInCanvas.js +129 -0
  101. package/dist/features/vite/definitionsSync.js +5 -6
  102. package/dist/features/vite/hostedChecks.js +127 -0
  103. package/dist/features/vite/runtimeDev.js +58 -17
  104. package/dist/features/vite/staticAssetHeaders.js +52 -0
  105. package/dist/features/vite/vite.d.ts +1 -1
  106. package/dist/features/vite/vite.js +49 -9
  107. package/dist/hooks/use-page-destinations.js +10 -10
  108. package/dist/lib/api-client-server.d.ts +1 -1
  109. package/dist/lib/api-client.d.ts +2 -1
  110. package/dist/lib/draft-scope.d.ts +1 -0
  111. package/dist/lib/locale-path.d.ts +3 -11
  112. package/dist/lib/locale-path.js +3 -27
  113. package/dist/lib/queries.js +7 -1
  114. package/dist/lib/revisions.js +20 -0
  115. package/dist/lib/shortcuts.js +29 -0
  116. package/dist/lib/studio-definitions.js +160 -0
  117. package/dist/lib/studio-reads.d.ts +1 -0
  118. package/dist/lib/studio-reads.js +65 -21
  119. package/dist/lib/use-project-room.js +16 -10
  120. package/dist/lib/utils.js +1 -104
  121. package/dist/package.js +1 -1
  122. package/dist/studio.css +1 -1
  123. package/package.json +9 -4
  124. package/skills/camox/SKILL.md +4 -0
  125. package/skills/camox/references/artifacts.md +1 -1
  126. package/dist/features/studio/components/FaviconDialog.js +0 -318
  127. package/dist/features/studio/components/SocialProfilesDialog.js +0 -342
@@ -6,13 +6,14 @@ 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
8
  import { asCollectionItem, recordMarkdownTemplate, recordPageValues } from "../../core/recordItem.js";
9
- import { buildLayoutContext, curatedLayoutContext } from "../../core/layoutContext.js";
10
9
  import { PLATFORM_SCRIPT } from "../../lib/platform.js";
11
- import { STUDIO_ASSETS_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, matchCollectionContentPath } from "../studio/routes.js";
10
+ import { STUDIO_ASSETS_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, STUDIO_SETTINGS_PATH, matchCollectionContentPath } from "../studio/routes.js";
12
11
  import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
13
12
  import { DefaultPageImage, renderPageImage } from "../og/pageImage.js";
14
13
  import { discoverLayoutPages, matchDerivedLayout } from "../../core/derivedRoutes.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
+ import { buildLayoutContext, curatedLayoutContext } from "../../core/layoutContext.js";
15
+ import { buildCamoxPageHead, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, declarePage, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readPageMarkdown, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter } from "../routes/pageRuntime.js";
16
+ import { assetPrefix } from "./assetPrefix.js";
16
17
  import { alternatesByPage, findAlternates, hreflangLinks } from "./alternates.js";
17
18
  import { configuredProviders, renderAnalyticsScripts } from "./analytics.js";
18
19
  import { omitArtifactSources } from "./artifactSources.js";
@@ -29,6 +30,13 @@ import { createHead, renderSSRHead } from "@unhead/react/server";
29
30
 
30
31
  //#region src/features/runtime/runtime.ts
31
32
  const DEFAULT_RUNTIME_BASE_PATH = "";
33
+ /** The studio's own routes, rather than a page or the Canvas. */
34
+ const STUDIO_ROUTE_KINDS = [
35
+ "studio",
36
+ "studio-content",
37
+ "studio-settings",
38
+ "studio-nested"
39
+ ];
32
40
  function normalizeRuntimeBasePath(basePath) {
33
41
  if (basePath === void 0) return DEFAULT_RUNTIME_BASE_PATH;
34
42
  if (!basePath || basePath === "/") return "";
@@ -57,6 +65,26 @@ function getRequestDraftId(request, options) {
57
65
  function isPrivateRender(request, options) {
58
66
  return !!getServerAuthCookieHeader(request.headers) || getRequestDraftId(request, options) !== void 0;
59
67
  }
68
+ /** The routes whose response is the same for every visitor, unless the request is private. */
69
+ const PUBLIC_ROUTE_KINDS = /* @__PURE__ */ new Set([
70
+ "page",
71
+ "data",
72
+ "og",
73
+ "logo",
74
+ "sitemap",
75
+ "llms-txt"
76
+ ]);
77
+ /**
78
+ * Whether a request reads what every visitor sees, so a shared cache may answer it: a GET or
79
+ * HEAD of a page or a public output, with no session, no draft named and no preview handoff. A
80
+ * response that comes out private all the same, such as the Studio's, still says so.
81
+ */
82
+ function isPublicRequest(request, options = {}) {
83
+ if (request.method !== "GET" && request.method !== "HEAD") return false;
84
+ if (isPrivateRender(request, options) || isPreviewHandoff(request, options)) return false;
85
+ const pathname = getRuntimePathname(request.url, options.runtimeBasePath);
86
+ return pathname !== null && PUBLIC_ROUTE_KINDS.has(matchRuntimeRoute(pathname).kind);
87
+ }
60
88
  function escapeXml(value) {
61
89
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
62
90
  }
@@ -90,6 +118,10 @@ function matchRuntimeRoute(pathname) {
90
118
  kind: "studio-content",
91
119
  pathname
92
120
  };
121
+ if (pathname === "/camox/settings" || pathname === `${"/camox/settings"}/`) return {
122
+ kind: "studio-settings",
123
+ pathname
124
+ };
93
125
  if (pathname.startsWith("/camox/")) return {
94
126
  kind: "studio-nested",
95
127
  pathname
@@ -184,10 +216,7 @@ function createLlmsTxtResponse(request, options) {
184
216
  pages: website.localizations[0].pages,
185
217
  layouts: (website.app?.getLayouts() ?? []).map(({ _internal }) => _internal),
186
218
  url: absoluteUrl(request, options)
187
- }), {
188
- "Content-Type": "text/plain; charset=utf-8",
189
- "Cache-Control": "public, max-age=300, stale-while-revalidate=3600"
190
- }));
219
+ }), { "Content-Type": "text/plain; charset=utf-8" }));
191
220
  }
192
221
  /** Tells LLM tools fetching a page's HTML where the website's llms.txt is. */
193
222
  function llmsTxtLinkHeader(options) {
@@ -431,8 +460,9 @@ function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl, loca
431
460
  return renderSSRHead(head);
432
461
  }
433
462
  /**
434
- * The browser loads a page's blocks before hydrating it. Preloading them alongside the entry
435
- * script avoids a request waterfall.
463
+ * A visitor's browser loads a page's blocks before hydrating it. Preloading them alongside the
464
+ * entry script avoids a request waterfall. A member's document loads none: only its renderer
465
+ * frame runs the site's code.
436
466
  */
437
467
  function renderBlockPreloads(options, dehydratedState) {
438
468
  const preloads = options.blockPreloads;
@@ -442,24 +472,40 @@ function renderBlockPreloads(options, dehydratedState) {
442
472
  const modulePath = preloads.modulePaths[type];
443
473
  if (!modulePath) continue;
444
474
  if (!preloads.chunks) urls.add(modulePath);
445
- for (const chunk of preloads.chunks?.[modulePath] ?? []) urls.add(`/${chunk}`);
475
+ for (const chunk of preloads.chunks?.[modulePath] ?? []) urls.add(`${assetPrefix()}${chunk}`);
446
476
  }
447
477
  return [...urls].map((url) => `<link rel="modulepreload" href="${escapeXml(url)}">`).join("");
448
478
  }
479
+ /**
480
+ * The renderer frame's document (ADR 0023): an empty document running the renderer's client,
481
+ * which renders the studio's pages into its frames, reading them itself. It holds no content, but
482
+ * is only ever the studio's, so no shared cache keeps it.
483
+ */
484
+ function createRendererDocumentResponse(options) {
485
+ const input = {
486
+ apiUrl: options.apiUrl,
487
+ authenticationUrl: options.authenticationUrl,
488
+ projectUuid: options.projectUuid,
489
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath)
490
+ };
491
+ return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta name="robots" content="noindex"><title>Camox renderer</title><script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(input)}<\/script></head><body>${renderClientEntryScript(options.rendererClientEntryUrl)}</body></html>`, { headers: {
492
+ "Content-Type": "text/html; charset=utf-8",
493
+ "Cache-Control": "private, no-store",
494
+ "X-Robots-Tag": "noindex"
495
+ } });
496
+ }
449
497
  function renderClientEntryScript(src) {
450
498
  if (!src) return "";
451
499
  return `<script type="module" src="${escapeXml(src)}"><\/script>`;
452
500
  }
453
- async function preparePreviewDocument(input, options, document, content) {
501
+ /**
502
+ * The site document the studio's frames load: the site's head and an empty root, which the
503
+ * renderer frame renders the page into. The studio never renders the site's code itself.
504
+ */
505
+ function preparePreviewDocument(input, options, document) {
454
506
  const siteHead = renderHead(document, createPageHeadInput(input.head ?? {}), options.stylesheetUrl, void 0, input.locale);
455
507
  const href = new URL(withRuntimeBasePath(input.pathname, options.runtimeBasePath), input.href).href;
456
- const siteHtml = content ?? (options.renderPage ? await options.renderPage({
457
- ...input,
458
- href,
459
- presentation: "public",
460
- previewDocument: void 0
461
- }) : "");
462
- input.previewDocument = `<!doctype html><html${siteHead.htmlAttrs}><head><base href="${escapeXml(href)}">${siteHead.headTags}</head><body${siteHead.bodyAttrs}>${siteHead.bodyTagsOpen}<div id="root" data-camox-preview-root>${siteHtml}</div>${siteHead.bodyTags}</body></html>`;
508
+ input.previewDocument = `<!doctype html><html${siteHead.htmlAttrs}><head><base href="${escapeXml(href)}">${siteHead.headTags}</head><body${siteHead.bodyAttrs}>${siteHead.bodyTagsOpen}<div id="root" data-camox-preview-root></div>${siteHead.bodyTags}</body></html>`;
463
509
  }
464
510
  /**
465
511
  * The render input a page's HTML embeds for hydration. Visitors load public pages, whose HTML
@@ -492,8 +538,7 @@ function isPreviewHandoff(request, options) {
492
538
  * The alternates of the page a request renders, where it exists in each of the scope's locales,
493
539
  * and its head with their `hreflang` links. Without a head, there is none to add them to.
494
540
  */
495
- async function readAlternates(request, options, { located, identity, head, described, ...reading }) {
496
- const localeReads = requestLocaleReads(options, reading);
541
+ async function readAlternates(request, options, { located, identity, head, described, localeReads }) {
497
542
  const [alternates, crumbs] = await Promise.all([findRequestAlternates(options, located, identity, localeReads), head ? readRequestBreadcrumb(options, located, described.title, localeReads) : []]);
498
543
  const url = absoluteUrl(request, options);
499
544
  const links = hreflangLinks(alternates, located.locales, url);
@@ -565,7 +610,7 @@ function withLocatedTags(head, { url, locale, alternates }) {
565
610
  }]
566
611
  };
567
612
  }
568
- function requestLocaleReads(options, { authCookieHeader, draftId }) {
613
+ function requestLocaleReads(options, { authCookieHeader, draftId, queryClient }) {
569
614
  const { apiUrl, projectUuid } = options;
570
615
  const reading = {
571
616
  authCookieHeader: draftId ? authCookieHeader : void 0,
@@ -573,9 +618,15 @@ function requestLocaleReads(options, { authCookieHeader, draftId }) {
573
618
  };
574
619
  const source = draftId ? "draft" : "live";
575
620
  const pagesByLocale = /* @__PURE__ */ new Map();
621
+ const studioCache = apiUrl && queryClient ? createServerStudioCache(queryClient, { apiUrl }) : void 0;
576
622
  return {
577
623
  pages: (locale) => {
578
624
  if (!apiUrl || !projectUuid) return Promise.resolve([]);
625
+ const loaded = studioCache?.get(reads.pageList({ projectUuid }), {
626
+ in: draftId ?? null,
627
+ locale
628
+ });
629
+ if (loaded) return Promise.resolve(loaded);
579
630
  const read = pagesByLocale.get(locale.tag) ?? createServerApiClient(apiUrl, {
580
631
  ...reading,
581
632
  locale
@@ -670,27 +721,89 @@ async function createPreviewHandoffResponse({ options, pathname, request }) {
670
721
  "Referrer-Policy": "no-referrer"
671
722
  } });
672
723
  }
673
- async function createPageHtmlResponse({ options, pathname, located, request }) {
724
+ /** Starts reading the curated page at a locale-relative path, as the request reads it. */
725
+ function readCuratedPage(options, request, { locale, path }) {
726
+ const queryClient = new QueryClient();
727
+ return {
728
+ queryClient,
729
+ result: loadCamoxPageForRequest({
730
+ apiUrl: options.apiUrl,
731
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
732
+ draftId: getRequestDraftId(request, options),
733
+ locale,
734
+ origin: new URL(request.url).origin,
735
+ pathname: path,
736
+ projectUuid: options.projectUuid,
737
+ queryClient
738
+ })
739
+ };
740
+ }
741
+ /** A first path segment that could be a locale's prefix: a language tag, in lowercase. */
742
+ const LOCALE_PREFIX_LIKE = /^\/[a-z]{2,3}(?:-[a-z0-9]{1,8})*(?:\/|$)/;
743
+ /**
744
+ * The read of the curated page a path names in the default locale, started while the request's
745
+ * locales are read, or none where it couldn't be the request's page. A default-locale page sits
746
+ * at its own path, so its read needn't wait for the locales: once read, they only confirm the path
747
+ * names no other locale. A path whose first segment looks like a locale's prefix waits for them
748
+ * instead, rather than reading a page it likely doesn't name, and a singleton's path reads no
749
+ * curated page.
750
+ */
751
+ async function readDefaultLocalePageEarly(options, request, pathname) {
752
+ const { apiUrl, projectUuid } = options;
753
+ if (!apiUrl || !projectUuid) return void 0;
754
+ if (matchLocalizedRoute(pathname)?.kind !== "page" || LOCALE_PREFIX_LIKE.test(pathname)) return;
755
+ const app = await options.getCamoxApp?.().catch(() => void 0);
756
+ if (app && matchDerivedLayout(app.getLayouts(), pathname)?.layout._internal.kind === "singleton") return void 0;
757
+ const read = readCuratedPage({
758
+ ...options,
759
+ apiUrl,
760
+ projectUuid
761
+ }, request, {
762
+ locale: UNNAMED_DEFAULT_LOCALE,
763
+ path: pathname
764
+ });
765
+ read.result.catch(() => void 0);
766
+ return read;
767
+ }
768
+ /**
769
+ * The reads of each locale the head of the request's curated page makes, and those it can make
770
+ * while the page is read. When the request reads Live, whatever the page's read finds about its
771
+ * draft, the pages of the locales the page's read doesn't load start loading now. Otherwise they
772
+ * wait for the page's read to settle the draft.
773
+ */
774
+ function curatedPageLocaleReads(options, request, located, queryClient) {
775
+ const authCookieHeader = getServerAuthCookieHeader(request.headers);
776
+ const forDraft = (draftId) => requestLocaleReads(options, {
777
+ authCookieHeader,
778
+ draftId,
779
+ queryClient
780
+ });
781
+ if (!(!getRequestDraftId(request, options) || !authCookieHeader)) return forDraft;
782
+ const live = forDraft(void 0);
783
+ if (located.locales.length > 1) for (const locale of located.locales) {
784
+ if (locale.tag === located.locale.tag || locale.isDefault) continue;
785
+ live.pages(locale).catch(() => void 0);
786
+ }
787
+ return () => live;
788
+ }
789
+ async function createPageHtmlResponse({ options, pathname, located, request, pageRead }) {
674
790
  if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !options.renderPage) return createPageDataResponse({
675
791
  options,
676
792
  pathname,
677
793
  located,
678
- request
794
+ request,
795
+ pageRead
679
796
  });
680
797
  const singleton = await createDerivedResponse(options, pathname, located, request, { singletonOnly: true });
681
798
  if (singleton) return singleton;
682
- const queryClient = new QueryClient();
799
+ const { queryClient, result: reading } = pageRead ?? readCuratedPage({
800
+ ...options,
801
+ apiUrl: options.apiUrl,
802
+ projectUuid: options.projectUuid
803
+ }, request, located);
804
+ const localeReads = curatedPageLocaleReads(options, request, located, queryClient);
683
805
  try {
684
- const result = await loadCamoxPageForRequest({
685
- apiUrl: options.apiUrl,
686
- authCookieHeader: getServerAuthCookieHeader(request.headers),
687
- draftId: getRequestDraftId(request, options),
688
- locale: located.locale,
689
- origin: new URL(request.url).origin,
690
- pathname: located.path,
691
- projectUuid: options.projectUuid,
692
- queryClient
693
- });
806
+ const result = await reading;
694
807
  const headers = new Headers({
695
808
  "Content-Type": "text/html; charset=utf-8",
696
809
  Vary: "Cookie",
@@ -708,8 +821,7 @@ async function createPageHtmlResponse({ options, pathname, located, request }) {
708
821
  },
709
822
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : {},
710
823
  described: curatedPageDescription(camoxApp, result.data.page, located.locale),
711
- authCookieHeader: getServerAuthCookieHeader(request.headers),
712
- draftId: result.draftId
824
+ localeReads: localeReads(result.draftId)
713
825
  });
714
826
  const dehydratedState = dehydrate(queryClient);
715
827
  const { project } = result;
@@ -738,7 +850,7 @@ async function createPageHtmlResponse({ options, pathname, located, request }) {
738
850
  locale: located.locale,
739
851
  locales: located.locales
740
852
  };
741
- await preparePreviewDocument(pageRenderInput, options, document, studio ? void 0 : "");
853
+ preparePreviewDocument(pageRenderInput, options, document);
742
854
  const appHtml = await options.renderPage(pageRenderInput);
743
855
  const renderedHead = renderHead(studio ? {} : document, createPageHeadInput(head), studio ? void 0 : options.stylesheetUrl, studio ? options.studioStylesheetUrl : void 0, studio ? void 0 : located.locale);
744
856
  return new Response(`<!doctype html>
@@ -747,7 +859,7 @@ async function createPageHtmlResponse({ options, pathname, located, request }) {
747
859
  ${renderStudioThemeScript(studio)}
748
860
  ${renderedHead.headTags}
749
861
  ${renderAnalyticsScripts(analytics)}
750
- ${renderBlockPreloads(options, dehydratedState)}
862
+ ${studio ? "" : renderBlockPreloads(options, dehydratedState)}
751
863
  <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(pageRenderInput, appHtml))}<\/script>
752
864
  </head>
753
865
  <body${renderedHead.bodyAttrs}>
@@ -982,8 +1094,10 @@ async function createDerivedResponse(options, pathname, located, request, { data
982
1094
  titles: recordValues?.titles
983
1095
  })
984
1096
  },
985
- authCookieHeader,
986
- draftId: read.draftId
1097
+ localeReads: requestLocaleReads(options, {
1098
+ authCookieHeader,
1099
+ draftId: read.draftId
1100
+ })
987
1101
  });
988
1102
  const analytics = dataOnly ? void 0 : visitorAnalytics(options, {
989
1103
  studio,
@@ -1020,15 +1134,15 @@ async function createDerivedResponse(options, pathname, located, request, { data
1020
1134
  }
1021
1135
  };
1022
1136
  const document = await options.getDocument?.() ?? {};
1023
- await preparePreviewDocument(input, options, document, !studio && !dataOnly ? "" : void 0);
1137
+ preparePreviewDocument(input, options, document);
1024
1138
  if (dataOnly) return Response.json(input, { headers: {
1025
- "Cache-Control": "private, no-store",
1139
+ ...isPrivateRender(request, options) ? { "Cache-Control": "private, no-store" } : {},
1026
1140
  Vary: "Cookie",
1027
1141
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
1028
1142
  } });
1029
1143
  const html = await options.renderPage(input);
1030
1144
  const head = renderHead(studio ? {} : document, createPageHeadInput(pageHead), studio ? void 0 : options.stylesheetUrl, studio ? options.studioStylesheetUrl : void 0, studio ? void 0 : located.locale);
1031
- return new Response(`<!doctype html><html${head.htmlAttrs}><head>${renderStudioThemeScript(studio)}${head.headTags}${renderAnalyticsScripts(analytics)}${renderBlockPreloads(options, input.dehydratedState)}<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(input, html))}<\/script></head><body${head.bodyAttrs}>${head.bodyTagsOpen}<div id="root">${html}</div>${head.bodyTags}${renderClientEntryScript(options.pageClientEntryUrl)}</body></html>`, { headers: {
1145
+ return new Response(`<!doctype html><html${head.htmlAttrs}><head>${renderStudioThemeScript(studio)}${head.headTags}${renderAnalyticsScripts(analytics)}${studio ? "" : renderBlockPreloads(options, input.dehydratedState)}<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(input, html))}<\/script></head><body${head.bodyAttrs}>${head.bodyTagsOpen}<div id="root">${html}</div>${head.bodyTags}${renderClientEntryScript(options.pageClientEntryUrl)}</body></html>`, { headers: {
1032
1146
  "Content-Type": "text/html; charset=utf-8",
1033
1147
  Vary: "Cookie",
1034
1148
  Link: llmsTxtLinkHeader(options),
@@ -1136,30 +1250,26 @@ async function createCanvasOnlyResponse({ options, pathname, located, request, d
1136
1250
  ...headers
1137
1251
  } });
1138
1252
  }
1139
- async function createPageDataResponse({ options, pathname, located, request }) {
1253
+ async function createPageDataResponse({ options, pathname, located, request, pageRead }) {
1140
1254
  if (!options.apiUrl || !options.projectUuid) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
1141
1255
  const singleton = await createDerivedResponse(options, pathname, located, request, {
1142
1256
  dataOnly: true,
1143
1257
  singletonOnly: true
1144
1258
  });
1145
1259
  if (singleton) return singleton;
1146
- const queryClient = new QueryClient();
1260
+ const { queryClient, result: reading } = pageRead ?? readCuratedPage({
1261
+ ...options,
1262
+ apiUrl: options.apiUrl,
1263
+ projectUuid: options.projectUuid
1264
+ }, request, located);
1265
+ const localeReads = curatedPageLocaleReads(options, request, located, queryClient);
1147
1266
  try {
1148
- const result = await loadCamoxPageForRequest({
1149
- apiUrl: options.apiUrl,
1150
- authCookieHeader: getServerAuthCookieHeader(request.headers),
1151
- draftId: getRequestDraftId(request, options),
1152
- locale: located.locale,
1153
- origin: new URL(request.url).origin,
1154
- pathname: located.path,
1155
- projectUuid: options.projectUuid,
1156
- queryClient
1157
- });
1267
+ const result = await reading;
1158
1268
  const headers = new Headers({
1159
1269
  "Content-Type": "application/json; charset=utf-8",
1160
- "Cache-Control": "private, no-store",
1161
1270
  Vary: "Cookie"
1162
1271
  });
1272
+ if (isPrivateRender(request, options)) headers.set("Cache-Control", "private, no-store");
1163
1273
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
1164
1274
  const camoxApp = await options.getCamoxApp?.();
1165
1275
  const { alternates, head } = await readAlternates(request, options, {
@@ -1170,8 +1280,7 @@ async function createPageDataResponse({ options, pathname, located, request }) {
1170
1280
  },
1171
1281
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null,
1172
1282
  described: curatedPageDescription(camoxApp, result.data.page, located.locale),
1173
- authCookieHeader: getServerAuthCookieHeader(request.headers),
1174
- draftId: result.draftId
1283
+ localeReads: localeReads(result.draftId)
1175
1284
  });
1176
1285
  const dehydratedState = dehydrate(queryClient);
1177
1286
  const input = {
@@ -1193,7 +1302,7 @@ async function createPageDataResponse({ options, pathname, located, request }) {
1193
1302
  head,
1194
1303
  dehydratedState
1195
1304
  };
1196
- await preparePreviewDocument(input, options, await options.getDocument?.() ?? {});
1305
+ preparePreviewDocument(input, options, await options.getDocument?.() ?? {});
1197
1306
  return Response.json(input, { headers });
1198
1307
  } catch (error) {
1199
1308
  if (isNotFoundError(error)) {
@@ -1222,6 +1331,7 @@ async function handleCamoxRequest(request, options = {}) {
1222
1331
  });
1223
1332
  }
1224
1333
  if (match.kind === "og") return createOgResponse(request, options);
1334
+ if (match.kind === "renderer") return createRendererDocumentResponse(options);
1225
1335
  if (match.kind === "logo") return createProjectLogoResponse(request, options);
1226
1336
  if (match.kind === "sitemap") return createSitemapResponse(request, options);
1227
1337
  if (match.kind === "llms-txt") return createLlmsTxtResponse(request, options);
@@ -1239,19 +1349,17 @@ async function handleCamoxRequest(request, options = {}) {
1239
1349
  const { apiUrl, projectUuid } = options;
1240
1350
  const markdownPathname = parseMarkdownPath(pathname);
1241
1351
  if (apiUrl && projectUuid && (markdownPathname || prefersMarkdown(request.headers.get("Accept") ?? ""))) {
1242
- const located = await locateRequest(request, options, markdownPathname ?? pathname, { live: true });
1243
- if (markdownPathname || matchLocalizedRoute(located.path)?.kind === "page") {
1244
- const markdownResponse = await createPageMarkdownResponse({
1245
- ...options,
1246
- apiUrl,
1247
- projectUuid
1248
- }, located, {
1249
- pathname: markdownPathname ?? pathname,
1250
- request
1251
- });
1252
- if (markdownResponse) return markdownResponse;
1253
- if (markdownPathname) return new Response("Page not found", { status: 404 });
1254
- }
1352
+ const markdownResponse = await createPageMarkdownResponse({
1353
+ ...options,
1354
+ apiUrl,
1355
+ projectUuid
1356
+ }, {
1357
+ pathname: markdownPathname ?? pathname,
1358
+ request,
1359
+ pagesOnly: !markdownPathname
1360
+ });
1361
+ if (markdownResponse) return markdownResponse;
1362
+ if (markdownPathname) return new Response("Page not found", { status: 404 });
1255
1363
  }
1256
1364
  return respondInLocale(options, request, pathname);
1257
1365
  }
@@ -1260,7 +1368,7 @@ async function handleCamoxRequest(request, options = {}) {
1260
1368
  * With `dataOnly`, as the JSON client navigation loads.
1261
1369
  */
1262
1370
  async function respondInLocale(options, request, pathname, { dataOnly = false } = {}) {
1263
- const located = await locateRequest(request, options, pathname);
1371
+ const [early, located] = await Promise.all([readDefaultLocalePageEarly(options, request, pathname), locateRequest(request, options, pathname)]);
1264
1372
  const localized = matchLocalizedRoute(located.path);
1265
1373
  if (!localized) return new Response("Not found", { status: 404 });
1266
1374
  const target = {
@@ -1278,13 +1386,29 @@ async function respondInLocale(options, request, pathname, { dataOnly = false }
1278
1386
  match: localized,
1279
1387
  dataOnly
1280
1388
  });
1281
- return dataOnly ? createPageDataResponse(target) : createPageHtmlResponse(target);
1389
+ const page = {
1390
+ ...target,
1391
+ pageRead: located.locale.isDefault ? early : void 0
1392
+ };
1393
+ return dataOnly ? createPageDataResponse(page) : createPageHtmlResponse(page);
1282
1394
  }
1283
1395
  /**
1284
1396
  * A page's Markdown Representation in its locale, with the same precedence as its HTML:
1285
- * singleton, curated page, then derived layout. Null when the page has none.
1397
+ * singleton, curated page, then derived layout. Null when the page has none. Markdown is a public
1398
+ * output: it reads Live, whoever asks, and one read finds the path's locale and its curated page.
1286
1399
  */
1287
- async function createPageMarkdownResponse(options, { locale, path }, { pathname, request }) {
1400
+ async function createPageMarkdownResponse(options, { pathname, request, pagesOnly }) {
1401
+ let read;
1402
+ try {
1403
+ read = await readPageMarkdown({
1404
+ ...options,
1405
+ path: pathname
1406
+ });
1407
+ } catch {
1408
+ return null;
1409
+ }
1410
+ const { locale, path } = read;
1411
+ if (pagesOnly && matchLocalizedRoute(path)?.kind !== "page") return null;
1288
1412
  const page = {
1289
1413
  pathname,
1290
1414
  url: absoluteUrl(request, options)(pathname)
@@ -1292,20 +1416,12 @@ async function createPageMarkdownResponse(options, { locale, path }, { pathname,
1292
1416
  const app = await options.getCamoxApp?.();
1293
1417
  const layoutMatch = app && matchDerivedLayout(app.getLayouts(), path);
1294
1418
  if (layoutMatch?.layout._internal.kind === "singleton") return createLayoutMarkdownResponse(options, layoutMatch, locale, page);
1295
- return createMarkdownResponse({
1296
- apiUrl: options.apiUrl,
1297
- camoxApp: app,
1298
- locale,
1299
- pathname: path,
1300
- projectUuid: options.projectUuid,
1301
- request,
1302
- url: page.url,
1303
- onNotFound: async () => layoutMatch ? createLayoutMarkdownResponse(options, layoutMatch, locale, page) : null
1304
- });
1419
+ if (read.page) return curatedMarkdownResponse(app, read, page.url);
1420
+ return layoutMatch ? createLayoutMarkdownResponse(options, layoutMatch, locale, page) : null;
1305
1421
  }
1306
1422
  /** Whether a route is one of the studio's own, rather than a page or the Canvas. */
1307
1423
  function isStudioRoute(match) {
1308
- return match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested";
1424
+ return STUDIO_ROUTE_KINDS.includes(match.kind);
1309
1425
  }
1310
1426
  /**
1311
1427
  * The studio or page route a locale-relative path names, or null for a runtime-owned route
@@ -1317,19 +1433,16 @@ function matchLocalizedRoute(path) {
1317
1433
  }
1318
1434
  /**
1319
1435
  * The locale a request's path names, among the locales of the scope it reads: the URL's draft's
1320
- * for a session naming one, else Live's. `live` reads Live's whatever the request names. Without
1321
- * an API there are none to read: every path is the default locale's, which then says it can't
1322
- * render.
1436
+ * for a session naming one, else Live's. Without an API there are none to read: every path is the
1437
+ * default locale's, which then says it can't render.
1323
1438
  */
1324
- async function locateRequest(request, options, pathname, { live = false } = {}) {
1439
+ async function locateRequest(request, options, pathname) {
1325
1440
  const { apiUrl, projectUuid } = options;
1326
1441
  const locales = apiUrl && projectUuid ? await readScopeLocales({
1327
1442
  apiUrl,
1328
1443
  projectUuid,
1329
- ...!live && {
1330
- authCookieHeader: getServerAuthCookieHeader(request.headers),
1331
- draftId: getRequestDraftId(request, options)
1332
- }
1444
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
1445
+ draftId: getRequestDraftId(request, options)
1333
1446
  }) : [UNNAMED_DEFAULT_LOCALE];
1334
1447
  return {
1335
1448
  ...parseLocalePath(pathname, locales),
@@ -1338,4 +1451,4 @@ async function locateRequest(request, options, pathname, { live = false } = {})
1338
1451
  }
1339
1452
 
1340
1453
  //#endregion
1341
- export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, matchRuntimeRoute };
1454
+ export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, isPublicRequest, matchRuntimeRoute };
@@ -10,6 +10,8 @@ export declare const RUNTIME_ROUTE_PATHS: {
10
10
  readonly data: "/_camox/data";
11
11
  readonly health: "/_camox/health";
12
12
  readonly registry: "/_camox/registry";
13
+ /** The renderer frame's document (ADR 0023), which the studio loads to render its pages. */
14
+ readonly renderer: "/_camox/renderer";
13
15
  readonly routes: "/_camox/routes";
14
16
  readonly logo: "/_camox/logo.png";
15
17
  readonly og: "/og";
@@ -10,6 +10,8 @@ const RUNTIME_ROUTE_PATHS = {
10
10
  data: "/_camox/data",
11
11
  health: "/_camox/health",
12
12
  registry: "/_camox/registry",
13
+ /** The renderer frame's document (ADR 0023), which the studio loads to render its pages. */
14
+ renderer: "/_camox/renderer",
13
15
  routes: "/_camox/routes",
14
16
  logo: "/_camox/logo.png",
15
17
  og: "/og",
@@ -1,6 +1,19 @@
1
+ import { WorkerEntrypoint } from "cloudflare:workers";
1
2
  //#region src/features/runtime/worker.d.ts
3
+ /** What the Worker gets with each request: only `waitUntil` matters here. */
4
+ interface WorkerContext {
5
+ waitUntil(promise: Promise<unknown>): void;
6
+ }
7
+ /**
8
+ * The website's Definition Set, for the Worker that loads a revision, which calls it by the name
9
+ * `DEFINITION_SET_ENTRYPOINT` holds. A named `WorkerEntrypoint` takes RPC calls only: no HTTP
10
+ * request reaches it.
11
+ */
12
+ export declare class CamoxDefinitionSet extends WorkerEntrypoint {
13
+ read(): Promise<unknown>;
14
+ }
2
15
  declare const _default: {
3
- fetch(request: Request): Promise<Response>;
16
+ fetch(request: Request, env?: unknown, context?: WorkerContext): Promise<Response>;
4
17
  };
5
18
  //#endregion
6
19
  export { _default as default };
@@ -1,6 +1,9 @@
1
+ import { rememberPublishingDraft } from "./publishingDraft.js";
1
2
  import { fetchLocalhostOnLoopback } from "./loopbackFetch.js";
3
+ import { PUBLIC_CACHE_NAME, readLiveCacheKey, respondThroughPublicCache } from "./publicCache.js";
2
4
  import { shouldHandleRequest, withDevHtml } from "./workerRequest.js";
3
- import { devHtml, handleCamoxRequest, runtimeBasePath } from "virtual:camox/server";
5
+ import { WorkerEntrypoint } from "cloudflare:workers";
6
+ import { devHtml, handleCamoxRequest, isPublicRequest, publicCacheSource, readDefinitionSet, runtimeBasePath } from "virtual:camox/server";
4
7
 
5
8
  //#region src/features/runtime/worker.ts
6
9
  /**
@@ -12,12 +15,39 @@ if (devHtml) globalThis.fetch = fetchLocalhostOnLoopback(globalThis.fetch.bind(g
12
15
  function notFound() {
13
16
  return new Response("Not found", { status: 404 });
14
17
  }
15
- var worker_default = { async fetch(request) {
18
+ /**
19
+ * Answers a request, from the edge cache when every visitor sees the same. Dev renders the
20
+ * developer's development draft, which nothing shares, and a Worker calling this one without
21
+ * its context can't keep anything past the response.
22
+ */
23
+ function respond(request, context) {
24
+ const caches = globalThis.caches;
25
+ if (devHtml || !context || !caches || !isPublicRequest(request)) return handleCamoxRequest(request);
26
+ const { apiUrl, projectUuid, build } = publicCacheSource;
27
+ return respondThroughPublicCache(request, {
28
+ cache: () => caches.open(PUBLIC_CACHE_NAME),
29
+ liveCacheKey: () => readLiveCacheKey(apiUrl, projectUuid),
30
+ build,
31
+ waitUntil: (promise) => context.waitUntil(promise)
32
+ }, () => handleCamoxRequest(request));
33
+ }
34
+ /**
35
+ * The website's Definition Set, for the Worker that loads a revision, which calls it by the name
36
+ * `DEFINITION_SET_ENTRYPOINT` holds. A named `WorkerEntrypoint` takes RPC calls only: no HTTP
37
+ * request reaches it.
38
+ */
39
+ var CamoxDefinitionSet = class extends WorkerEntrypoint {
40
+ read() {
41
+ return readDefinitionSet();
42
+ }
43
+ };
44
+ var worker_default = { async fetch(request, env, context) {
45
+ rememberPublishingDraft(env);
16
46
  if (!shouldHandleRequest(request, runtimeBasePath)) return notFound();
17
- const response = await handleCamoxRequest(request);
47
+ const response = await respond(request, context);
18
48
  if (!response) return notFound();
19
49
  return withDevHtml(response, devHtml);
20
50
  } };
21
51
 
22
52
  //#endregion
23
- export { worker_default as default };
53
+ export { CamoxDefinitionSet, worker_default as default };