camox 0.48.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.
Files changed (57) hide show
  1. package/dist/core/layoutContext.d.ts +1 -0
  2. package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
  3. package/dist/features/content/ContentAssets.js +84 -88
  4. package/dist/features/preview/CamoxPreview.js +1 -1
  5. package/dist/features/preview/EditablePageExperience.js +17 -9
  6. package/dist/features/preview/components/BlockActionsPopover.js +11 -35
  7. package/dist/features/preview/components/FieldToolbar.js +1 -1
  8. package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
  9. package/dist/features/preview/components/PreviewPanel.js +10 -64
  10. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  11. package/dist/features/preview/previewConstants.js +2 -8
  12. package/dist/features/preview/usePreviewViewportActions.js +1 -1
  13. package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
  14. package/dist/features/provider/components/CommandPalette.js +2 -5
  15. package/dist/features/provider/signInShortcut.js +1 -2
  16. package/dist/features/provider/useAdminShortcuts.js +47 -53
  17. package/dist/features/routes/pageRuntime.d.ts +2 -1
  18. package/dist/features/routes/pageRuntime.js +27 -20
  19. package/dist/features/runtime/publicCache.js +83 -0
  20. package/dist/features/runtime/runtime.d.ts +13 -3
  21. package/dist/features/runtime/runtime.js +173 -79
  22. package/dist/features/runtime/worker.d.ts +5 -1
  23. package/dist/features/runtime/worker.js +20 -3
  24. package/dist/features/settings/AnalyticsSettings.js +622 -0
  25. package/dist/features/settings/CamoxSettings.js +95 -0
  26. package/dist/features/settings/FaviconSettings.js +254 -0
  27. package/dist/features/settings/ProjectNameSettings.js +145 -0
  28. package/dist/features/settings/SettingsPart.js +73 -0
  29. package/dist/features/settings/SocialProfilesSettings.js +245 -0
  30. package/dist/features/settings/useProjectMutation.js +61 -0
  31. package/dist/features/studio/components/DraftsControl.js +136 -131
  32. package/dist/features/studio/components/LocalePicker.js +178 -174
  33. package/dist/features/studio/components/Navbar.js +107 -66
  34. package/dist/features/studio/components/ProjectMenu.js +118 -170
  35. package/dist/features/studio/components/ReviewDraftSheet.js +1046 -990
  36. package/dist/features/studio/components/UserButton.js +1 -1
  37. package/dist/features/studio/routes.js +3 -1
  38. package/dist/features/studio/showInCanvas.js +129 -0
  39. package/dist/features/vite/definitionsSync.js +4 -5
  40. package/dist/features/vite/runtimeDev.js +32 -17
  41. package/dist/features/vite/staticAssetHeaders.js +52 -0
  42. package/dist/features/vite/vite.d.ts +1 -1
  43. package/dist/features/vite/vite.js +16 -4
  44. package/dist/lib/api-client-server.d.ts +1 -1
  45. package/dist/lib/api-client.d.ts +2 -1
  46. package/dist/lib/draft-scope.d.ts +1 -0
  47. package/dist/lib/locale-path.d.ts +3 -11
  48. package/dist/lib/locale-path.js +3 -27
  49. package/dist/lib/queries.js +7 -1
  50. package/dist/lib/shortcuts.js +29 -0
  51. package/dist/lib/studio-reads.d.ts +1 -0
  52. package/dist/lib/utils.js +1 -104
  53. package/dist/package.js +1 -1
  54. package/dist/studio.css +1 -1
  55. package/package.json +5 -4
  56. package/dist/features/studio/components/FaviconDialog.js +0 -318
  57. package/dist/features/studio/components/SocialProfilesDialog.js +0 -342
@@ -8,11 +8,11 @@ import { reads } from "../../lib/studio-reads.js";
8
8
  import { asCollectionItem, recordMarkdownTemplate, recordPageValues } from "../../core/recordItem.js";
9
9
  import { buildLayoutContext, curatedLayoutContext } from "../../core/layoutContext.js";
10
10
  import { PLATFORM_SCRIPT } from "../../lib/platform.js";
11
- import { STUDIO_ASSETS_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, matchCollectionContentPath } from "../studio/routes.js";
11
+ import { STUDIO_ASSETS_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, STUDIO_SETTINGS_PATH, matchCollectionContentPath } from "../studio/routes.js";
12
12
  import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
13
13
  import { DefaultPageImage, renderPageImage } from "../og/pageImage.js";
14
14
  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";
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
16
  import { alternatesByPage, findAlternates, hreflangLinks } from "./alternates.js";
17
17
  import { configuredProviders, renderAnalyticsScripts } from "./analytics.js";
18
18
  import { omitArtifactSources } from "./artifactSources.js";
@@ -29,6 +29,13 @@ import { createHead, renderSSRHead } from "@unhead/react/server";
29
29
 
30
30
  //#region src/features/runtime/runtime.ts
31
31
  const DEFAULT_RUNTIME_BASE_PATH = "";
32
+ /** The studio's own routes, rather than a page or the Canvas. */
33
+ const STUDIO_ROUTE_KINDS = [
34
+ "studio",
35
+ "studio-content",
36
+ "studio-settings",
37
+ "studio-nested"
38
+ ];
32
39
  function normalizeRuntimeBasePath(basePath) {
33
40
  if (basePath === void 0) return DEFAULT_RUNTIME_BASE_PATH;
34
41
  if (!basePath || basePath === "/") return "";
@@ -57,6 +64,26 @@ function getRequestDraftId(request, options) {
57
64
  function isPrivateRender(request, options) {
58
65
  return !!getServerAuthCookieHeader(request.headers) || getRequestDraftId(request, options) !== void 0;
59
66
  }
67
+ /** The routes whose response is the same for every visitor, unless the request is private. */
68
+ const PUBLIC_ROUTE_KINDS = /* @__PURE__ */ new Set([
69
+ "page",
70
+ "data",
71
+ "og",
72
+ "logo",
73
+ "sitemap",
74
+ "llms-txt"
75
+ ]);
76
+ /**
77
+ * Whether a request reads what every visitor sees, so a shared cache may answer it: a GET or
78
+ * HEAD of a page or a public output, with no session, no draft named and no preview handoff. A
79
+ * response that comes out private all the same, such as the Studio's, still says so.
80
+ */
81
+ function isPublicRequest(request, options = {}) {
82
+ if (request.method !== "GET" && request.method !== "HEAD") return false;
83
+ if (isPrivateRender(request, options) || isPreviewHandoff(request, options)) return false;
84
+ const pathname = getRuntimePathname(request.url, options.runtimeBasePath);
85
+ return pathname !== null && PUBLIC_ROUTE_KINDS.has(matchRuntimeRoute(pathname).kind);
86
+ }
60
87
  function escapeXml(value) {
61
88
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
62
89
  }
@@ -90,6 +117,10 @@ function matchRuntimeRoute(pathname) {
90
117
  kind: "studio-content",
91
118
  pathname
92
119
  };
120
+ if (pathname === "/camox/settings" || pathname === `${"/camox/settings"}/`) return {
121
+ kind: "studio-settings",
122
+ pathname
123
+ };
93
124
  if (pathname.startsWith("/camox/")) return {
94
125
  kind: "studio-nested",
95
126
  pathname
@@ -184,10 +215,7 @@ function createLlmsTxtResponse(request, options) {
184
215
  pages: website.localizations[0].pages,
185
216
  layouts: (website.app?.getLayouts() ?? []).map(({ _internal }) => _internal),
186
217
  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
- }));
218
+ }), { "Content-Type": "text/plain; charset=utf-8" }));
191
219
  }
192
220
  /** Tells LLM tools fetching a page's HTML where the website's llms.txt is. */
193
221
  function llmsTxtLinkHeader(options) {
@@ -492,8 +520,7 @@ function isPreviewHandoff(request, options) {
492
520
  * The alternates of the page a request renders, where it exists in each of the scope's locales,
493
521
  * and its head with their `hreflang` links. Without a head, there is none to add them to.
494
522
  */
495
- async function readAlternates(request, options, { located, identity, head, described, ...reading }) {
496
- const localeReads = requestLocaleReads(options, reading);
523
+ async function readAlternates(request, options, { located, identity, head, described, localeReads }) {
497
524
  const [alternates, crumbs] = await Promise.all([findRequestAlternates(options, located, identity, localeReads), head ? readRequestBreadcrumb(options, located, described.title, localeReads) : []]);
498
525
  const url = absoluteUrl(request, options);
499
526
  const links = hreflangLinks(alternates, located.locales, url);
@@ -565,7 +592,7 @@ function withLocatedTags(head, { url, locale, alternates }) {
565
592
  }]
566
593
  };
567
594
  }
568
- function requestLocaleReads(options, { authCookieHeader, draftId }) {
595
+ function requestLocaleReads(options, { authCookieHeader, draftId, queryClient }) {
569
596
  const { apiUrl, projectUuid } = options;
570
597
  const reading = {
571
598
  authCookieHeader: draftId ? authCookieHeader : void 0,
@@ -573,9 +600,15 @@ function requestLocaleReads(options, { authCookieHeader, draftId }) {
573
600
  };
574
601
  const source = draftId ? "draft" : "live";
575
602
  const pagesByLocale = /* @__PURE__ */ new Map();
603
+ const studioCache = apiUrl && queryClient ? createServerStudioCache(queryClient, { apiUrl }) : void 0;
576
604
  return {
577
605
  pages: (locale) => {
578
606
  if (!apiUrl || !projectUuid) return Promise.resolve([]);
607
+ const loaded = studioCache?.get(reads.pageList({ projectUuid }), {
608
+ in: draftId ?? null,
609
+ locale
610
+ });
611
+ if (loaded) return Promise.resolve(loaded);
579
612
  const read = pagesByLocale.get(locale.tag) ?? createServerApiClient(apiUrl, {
580
613
  ...reading,
581
614
  locale
@@ -670,27 +703,89 @@ async function createPreviewHandoffResponse({ options, pathname, request }) {
670
703
  "Referrer-Policy": "no-referrer"
671
704
  } });
672
705
  }
673
- async function createPageHtmlResponse({ options, pathname, located, request }) {
706
+ /** Starts reading the curated page at a locale-relative path, as the request reads it. */
707
+ function readCuratedPage(options, request, { locale, path }) {
708
+ const queryClient = new QueryClient();
709
+ return {
710
+ queryClient,
711
+ result: loadCamoxPageForRequest({
712
+ apiUrl: options.apiUrl,
713
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
714
+ draftId: getRequestDraftId(request, options),
715
+ locale,
716
+ origin: new URL(request.url).origin,
717
+ pathname: path,
718
+ projectUuid: options.projectUuid,
719
+ queryClient
720
+ })
721
+ };
722
+ }
723
+ /** A first path segment that could be a locale's prefix: a language tag, in lowercase. */
724
+ const LOCALE_PREFIX_LIKE = /^\/[a-z]{2,3}(?:-[a-z0-9]{1,8})*(?:\/|$)/;
725
+ /**
726
+ * The read of the curated page a path names in the default locale, started while the request's
727
+ * locales are read, or none where it couldn't be the request's page. A default-locale page sits
728
+ * at its own path, so its read needn't wait for the locales: once read, they only confirm the path
729
+ * names no other locale. A path whose first segment looks like a locale's prefix waits for them
730
+ * instead, rather than reading a page it likely doesn't name, and a singleton's path reads no
731
+ * curated page.
732
+ */
733
+ async function readDefaultLocalePageEarly(options, request, pathname) {
734
+ const { apiUrl, projectUuid } = options;
735
+ if (!apiUrl || !projectUuid) return void 0;
736
+ if (matchLocalizedRoute(pathname)?.kind !== "page" || LOCALE_PREFIX_LIKE.test(pathname)) return;
737
+ const app = await options.getCamoxApp?.().catch(() => void 0);
738
+ if (app && matchDerivedLayout(app.getLayouts(), pathname)?.layout._internal.kind === "singleton") return void 0;
739
+ const read = readCuratedPage({
740
+ ...options,
741
+ apiUrl,
742
+ projectUuid
743
+ }, request, {
744
+ locale: UNNAMED_DEFAULT_LOCALE,
745
+ path: pathname
746
+ });
747
+ read.result.catch(() => void 0);
748
+ return read;
749
+ }
750
+ /**
751
+ * The reads of each locale the head of the request's curated page makes, and those it can make
752
+ * while the page is read. When the request reads Live, whatever the page's read finds about its
753
+ * draft, the pages of the locales the page's read doesn't load start loading now. Otherwise they
754
+ * wait for the page's read to settle the draft.
755
+ */
756
+ function curatedPageLocaleReads(options, request, located, queryClient) {
757
+ const authCookieHeader = getServerAuthCookieHeader(request.headers);
758
+ const forDraft = (draftId) => requestLocaleReads(options, {
759
+ authCookieHeader,
760
+ draftId,
761
+ queryClient
762
+ });
763
+ if (!(!getRequestDraftId(request, options) || !authCookieHeader)) return forDraft;
764
+ const live = forDraft(void 0);
765
+ if (located.locales.length > 1) for (const locale of located.locales) {
766
+ if (locale.tag === located.locale.tag || locale.isDefault) continue;
767
+ live.pages(locale).catch(() => void 0);
768
+ }
769
+ return () => live;
770
+ }
771
+ async function createPageHtmlResponse({ options, pathname, located, request, pageRead }) {
674
772
  if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !options.renderPage) return createPageDataResponse({
675
773
  options,
676
774
  pathname,
677
775
  located,
678
- request
776
+ request,
777
+ pageRead
679
778
  });
680
779
  const singleton = await createDerivedResponse(options, pathname, located, request, { singletonOnly: true });
681
780
  if (singleton) return singleton;
682
- const queryClient = new QueryClient();
781
+ const { queryClient, result: reading } = pageRead ?? readCuratedPage({
782
+ ...options,
783
+ apiUrl: options.apiUrl,
784
+ projectUuid: options.projectUuid
785
+ }, request, located);
786
+ const localeReads = curatedPageLocaleReads(options, request, located, queryClient);
683
787
  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
- });
788
+ const result = await reading;
694
789
  const headers = new Headers({
695
790
  "Content-Type": "text/html; charset=utf-8",
696
791
  Vary: "Cookie",
@@ -708,8 +803,7 @@ async function createPageHtmlResponse({ options, pathname, located, request }) {
708
803
  },
709
804
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : {},
710
805
  described: curatedPageDescription(camoxApp, result.data.page, located.locale),
711
- authCookieHeader: getServerAuthCookieHeader(request.headers),
712
- draftId: result.draftId
806
+ localeReads: localeReads(result.draftId)
713
807
  });
714
808
  const dehydratedState = dehydrate(queryClient);
715
809
  const { project } = result;
@@ -982,8 +1076,10 @@ async function createDerivedResponse(options, pathname, located, request, { data
982
1076
  titles: recordValues?.titles
983
1077
  })
984
1078
  },
985
- authCookieHeader,
986
- draftId: read.draftId
1079
+ localeReads: requestLocaleReads(options, {
1080
+ authCookieHeader,
1081
+ draftId: read.draftId
1082
+ })
987
1083
  });
988
1084
  const analytics = dataOnly ? void 0 : visitorAnalytics(options, {
989
1085
  studio,
@@ -1022,7 +1118,7 @@ async function createDerivedResponse(options, pathname, located, request, { data
1022
1118
  const document = await options.getDocument?.() ?? {};
1023
1119
  await preparePreviewDocument(input, options, document, !studio && !dataOnly ? "" : void 0);
1024
1120
  if (dataOnly) return Response.json(input, { headers: {
1025
- "Cache-Control": "private, no-store",
1121
+ ...isPrivateRender(request, options) ? { "Cache-Control": "private, no-store" } : {},
1026
1122
  Vary: "Cookie",
1027
1123
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
1028
1124
  } });
@@ -1136,30 +1232,26 @@ async function createCanvasOnlyResponse({ options, pathname, located, request, d
1136
1232
  ...headers
1137
1233
  } });
1138
1234
  }
1139
- async function createPageDataResponse({ options, pathname, located, request }) {
1235
+ async function createPageDataResponse({ options, pathname, located, request, pageRead }) {
1140
1236
  if (!options.apiUrl || !options.projectUuid) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
1141
1237
  const singleton = await createDerivedResponse(options, pathname, located, request, {
1142
1238
  dataOnly: true,
1143
1239
  singletonOnly: true
1144
1240
  });
1145
1241
  if (singleton) return singleton;
1146
- const queryClient = new QueryClient();
1242
+ const { queryClient, result: reading } = pageRead ?? readCuratedPage({
1243
+ ...options,
1244
+ apiUrl: options.apiUrl,
1245
+ projectUuid: options.projectUuid
1246
+ }, request, located);
1247
+ const localeReads = curatedPageLocaleReads(options, request, located, queryClient);
1147
1248
  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
- });
1249
+ const result = await reading;
1158
1250
  const headers = new Headers({
1159
1251
  "Content-Type": "application/json; charset=utf-8",
1160
- "Cache-Control": "private, no-store",
1161
1252
  Vary: "Cookie"
1162
1253
  });
1254
+ if (isPrivateRender(request, options)) headers.set("Cache-Control", "private, no-store");
1163
1255
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
1164
1256
  const camoxApp = await options.getCamoxApp?.();
1165
1257
  const { alternates, head } = await readAlternates(request, options, {
@@ -1170,8 +1262,7 @@ async function createPageDataResponse({ options, pathname, located, request }) {
1170
1262
  },
1171
1263
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null,
1172
1264
  described: curatedPageDescription(camoxApp, result.data.page, located.locale),
1173
- authCookieHeader: getServerAuthCookieHeader(request.headers),
1174
- draftId: result.draftId
1265
+ localeReads: localeReads(result.draftId)
1175
1266
  });
1176
1267
  const dehydratedState = dehydrate(queryClient);
1177
1268
  const input = {
@@ -1239,19 +1330,17 @@ async function handleCamoxRequest(request, options = {}) {
1239
1330
  const { apiUrl, projectUuid } = options;
1240
1331
  const markdownPathname = parseMarkdownPath(pathname);
1241
1332
  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
- }
1333
+ const markdownResponse = await createPageMarkdownResponse({
1334
+ ...options,
1335
+ apiUrl,
1336
+ projectUuid
1337
+ }, {
1338
+ pathname: markdownPathname ?? pathname,
1339
+ request,
1340
+ pagesOnly: !markdownPathname
1341
+ });
1342
+ if (markdownResponse) return markdownResponse;
1343
+ if (markdownPathname) return new Response("Page not found", { status: 404 });
1255
1344
  }
1256
1345
  return respondInLocale(options, request, pathname);
1257
1346
  }
@@ -1260,7 +1349,7 @@ async function handleCamoxRequest(request, options = {}) {
1260
1349
  * With `dataOnly`, as the JSON client navigation loads.
1261
1350
  */
1262
1351
  async function respondInLocale(options, request, pathname, { dataOnly = false } = {}) {
1263
- const located = await locateRequest(request, options, pathname);
1352
+ const [early, located] = await Promise.all([readDefaultLocalePageEarly(options, request, pathname), locateRequest(request, options, pathname)]);
1264
1353
  const localized = matchLocalizedRoute(located.path);
1265
1354
  if (!localized) return new Response("Not found", { status: 404 });
1266
1355
  const target = {
@@ -1278,13 +1367,29 @@ async function respondInLocale(options, request, pathname, { dataOnly = false }
1278
1367
  match: localized,
1279
1368
  dataOnly
1280
1369
  });
1281
- return dataOnly ? createPageDataResponse(target) : createPageHtmlResponse(target);
1370
+ const page = {
1371
+ ...target,
1372
+ pageRead: located.locale.isDefault ? early : void 0
1373
+ };
1374
+ return dataOnly ? createPageDataResponse(page) : createPageHtmlResponse(page);
1282
1375
  }
1283
1376
  /**
1284
1377
  * 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.
1378
+ * singleton, curated page, then derived layout. Null when the page has none. Markdown is a public
1379
+ * output: it reads Live, whoever asks, and one read finds the path's locale and its curated page.
1286
1380
  */
1287
- async function createPageMarkdownResponse(options, { locale, path }, { pathname, request }) {
1381
+ async function createPageMarkdownResponse(options, { pathname, request, pagesOnly }) {
1382
+ let read;
1383
+ try {
1384
+ read = await readPageMarkdown({
1385
+ ...options,
1386
+ path: pathname
1387
+ });
1388
+ } catch {
1389
+ return null;
1390
+ }
1391
+ const { locale, path } = read;
1392
+ if (pagesOnly && matchLocalizedRoute(path)?.kind !== "page") return null;
1288
1393
  const page = {
1289
1394
  pathname,
1290
1395
  url: absoluteUrl(request, options)(pathname)
@@ -1292,20 +1397,12 @@ async function createPageMarkdownResponse(options, { locale, path }, { pathname,
1292
1397
  const app = await options.getCamoxApp?.();
1293
1398
  const layoutMatch = app && matchDerivedLayout(app.getLayouts(), path);
1294
1399
  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
- });
1400
+ if (read.page) return curatedMarkdownResponse(app, read, page.url);
1401
+ return layoutMatch ? createLayoutMarkdownResponse(options, layoutMatch, locale, page) : null;
1305
1402
  }
1306
1403
  /** Whether a route is one of the studio's own, rather than a page or the Canvas. */
1307
1404
  function isStudioRoute(match) {
1308
- return match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested";
1405
+ return STUDIO_ROUTE_KINDS.includes(match.kind);
1309
1406
  }
1310
1407
  /**
1311
1408
  * The studio or page route a locale-relative path names, or null for a runtime-owned route
@@ -1317,19 +1414,16 @@ function matchLocalizedRoute(path) {
1317
1414
  }
1318
1415
  /**
1319
1416
  * 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.
1417
+ * for a session naming one, else Live's. Without an API there are none to read: every path is the
1418
+ * default locale's, which then says it can't render.
1323
1419
  */
1324
- async function locateRequest(request, options, pathname, { live = false } = {}) {
1420
+ async function locateRequest(request, options, pathname) {
1325
1421
  const { apiUrl, projectUuid } = options;
1326
1422
  const locales = apiUrl && projectUuid ? await readScopeLocales({
1327
1423
  apiUrl,
1328
1424
  projectUuid,
1329
- ...!live && {
1330
- authCookieHeader: getServerAuthCookieHeader(request.headers),
1331
- draftId: getRequestDraftId(request, options)
1332
- }
1425
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
1426
+ draftId: getRequestDraftId(request, options)
1333
1427
  }) : [UNNAMED_DEFAULT_LOCALE];
1334
1428
  return {
1335
1429
  ...parseLocalePath(pathname, locales),
@@ -1338,4 +1432,4 @@ async function locateRequest(request, options, pathname, { live = false } = {})
1338
1432
  }
1339
1433
 
1340
1434
  //#endregion
1341
- export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, matchRuntimeRoute };
1435
+ export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, isPublicRequest, matchRuntimeRoute };
@@ -1,6 +1,10 @@
1
1
  //#region src/features/runtime/worker.d.ts
2
+ /** What the Worker gets with each request: only `waitUntil` matters here. */
3
+ interface WorkerContext {
4
+ waitUntil(promise: Promise<unknown>): void;
5
+ }
2
6
  declare const _default: {
3
- fetch(request: Request): Promise<Response>;
7
+ fetch(request: Request, _env?: unknown, context?: WorkerContext): Promise<Response>;
4
8
  };
5
9
  //#endregion
6
10
  export { _default as default };
@@ -1,6 +1,7 @@
1
1
  import { fetchLocalhostOnLoopback } from "./loopbackFetch.js";
2
+ import { readLiveCacheKey, respondThroughPublicCache } from "./publicCache.js";
2
3
  import { shouldHandleRequest, withDevHtml } from "./workerRequest.js";
3
- import { devHtml, handleCamoxRequest, runtimeBasePath } from "virtual:camox/server";
4
+ import { devHtml, handleCamoxRequest, isPublicRequest, publicCacheSource, runtimeBasePath } from "virtual:camox/server";
4
5
 
5
6
  //#region src/features/runtime/worker.ts
6
7
  /**
@@ -12,9 +13,25 @@ if (devHtml) globalThis.fetch = fetchLocalhostOnLoopback(globalThis.fetch.bind(g
12
13
  function notFound() {
13
14
  return new Response("Not found", { status: 404 });
14
15
  }
15
- var worker_default = { async fetch(request) {
16
+ /**
17
+ * Answers a request, from the edge cache when every visitor sees the same. Dev renders the
18
+ * developer's development draft, which nothing shares, and a Worker calling this one without
19
+ * its context can't keep anything past the response.
20
+ */
21
+ function respond(request, context) {
22
+ const cache = globalThis.caches?.default;
23
+ if (devHtml || !context || !cache || !isPublicRequest(request)) return handleCamoxRequest(request);
24
+ const { apiUrl, projectUuid, build } = publicCacheSource;
25
+ return respondThroughPublicCache(request, {
26
+ cache,
27
+ liveCacheKey: () => readLiveCacheKey(apiUrl, projectUuid),
28
+ build,
29
+ waitUntil: (promise) => context.waitUntil(promise)
30
+ }, () => handleCamoxRequest(request));
31
+ }
32
+ var worker_default = { async fetch(request, _env, context) {
16
33
  if (!shouldHandleRequest(request, runtimeBasePath)) return notFound();
17
- const response = await handleCamoxRequest(request);
34
+ const response = await respond(request, context);
18
35
  if (!response) return notFound();
19
36
  return withDevHtml(response, devHtml);
20
37
  } };