camox 0.47.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) 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 +55 -0
  15. package/dist/core/layoutContext.js +66 -0
  16. package/dist/core/lib/contentType.d.ts +91 -4
  17. package/dist/core/lib/contentType.js +84 -7
  18. package/dist/core/lib/recordScope.js +19 -17
  19. package/dist/core/lib/reference.d.ts +6 -2
  20. package/dist/core/recordItem.js +55 -2
  21. package/dist/core/requestContext.d.ts +18 -0
  22. package/dist/core/structuredData.d.ts +126 -0
  23. package/dist/core/structuredData.js +40 -0
  24. package/dist/features/canvas/canvasCamera.js +1 -1
  25. package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
  26. package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
  27. package/dist/features/content/ContentAssets.js +84 -88
  28. package/dist/features/content/ContentCollection.js +13 -7
  29. package/dist/features/content/collection-form.js +6 -2
  30. package/dist/features/content/components/MissingFromLocale.js +1 -1
  31. package/dist/features/og/pageImage.js +89 -0
  32. package/dist/features/page/CuratedPageContent.js +45 -32
  33. package/dist/features/page/DerivedPageContent.js +49 -22
  34. package/dist/features/page/SharedLayoutContent.js +26 -22
  35. package/dist/features/preview/CamoxPreview.js +41 -27
  36. package/dist/features/preview/EditablePageExperience.js +17 -9
  37. package/dist/features/preview/components/AddBlockSheet.js +1 -0
  38. package/dist/features/preview/components/BlockActionsPopover.js +19 -41
  39. package/dist/features/preview/components/FieldToolbar.js +1 -1
  40. package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
  41. package/dist/features/preview/components/PageInfo.js +244 -264
  42. package/dist/features/preview/components/PageTree.js +63 -53
  43. package/dist/features/preview/components/PreviewPanel.js +10 -64
  44. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  45. package/dist/features/preview/components/RecordCard.js +50 -34
  46. package/dist/features/preview/components/RecordCombobox.js +12 -5
  47. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
  48. package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
  49. package/dist/features/preview/components/RepeatableItemsList.js +62 -46
  50. package/dist/features/preview/components/SelectionEditor.js +3 -2
  51. package/dist/features/preview/components/useFieldWriter.js +2 -4
  52. package/dist/features/preview/components/useRecordView.js +2 -2
  53. package/dist/features/preview/entityName.js +11 -0
  54. package/dist/features/preview/pageRecord.js +17 -18
  55. package/dist/features/preview/previewConstants.js +2 -8
  56. package/dist/features/preview/usePreviewViewportActions.js +1 -1
  57. package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
  58. package/dist/features/provider/components/CommandPalette.js +2 -5
  59. package/dist/features/provider/signInShortcut.js +1 -2
  60. package/dist/features/provider/useAdminShortcuts.js +47 -53
  61. package/dist/features/routes/pageRuntime.d.ts +17 -2
  62. package/dist/features/routes/pageRuntime.js +61 -57
  63. package/dist/features/runtime/RuntimeChrome.js +1 -1
  64. package/dist/features/runtime/breadcrumbs.js +68 -0
  65. package/dist/features/runtime/livePages.js +3 -2
  66. package/dist/features/runtime/pageGraph.js +92 -0
  67. package/dist/features/runtime/projectLogo.js +110 -0
  68. package/dist/features/runtime/publicCache.js +83 -0
  69. package/dist/features/runtime/runtime.d.ts +24 -9
  70. package/dist/features/runtime/runtime.js +447 -130
  71. package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
  72. package/dist/features/runtime/runtimeRoutes.js +1 -0
  73. package/dist/features/runtime/structuredData.js +213 -0
  74. package/dist/features/runtime/worker.d.ts +5 -1
  75. package/dist/features/runtime/worker.js +20 -3
  76. package/dist/features/settings/AnalyticsSettings.js +622 -0
  77. package/dist/features/settings/CamoxSettings.js +95 -0
  78. package/dist/features/settings/FaviconSettings.js +254 -0
  79. package/dist/features/settings/ProjectNameSettings.js +145 -0
  80. package/dist/features/settings/SettingsPart.js +73 -0
  81. package/dist/features/settings/SocialProfilesSettings.js +245 -0
  82. package/dist/features/settings/useProjectMutation.js +61 -0
  83. package/dist/features/studio/components/ChatSidebar.js +5 -5
  84. package/dist/features/studio/components/DraftsControl.js +136 -131
  85. package/dist/features/studio/components/LocalePicker.js +178 -174
  86. package/dist/features/studio/components/Navbar.js +107 -66
  87. package/dist/features/studio/components/ProjectMenu.js +85 -110
  88. package/dist/features/studio/components/ReviewDraftSheet.js +1046 -990
  89. package/dist/features/studio/components/ThreadMessages.js +2 -2
  90. package/dist/features/studio/components/UserButton.js +1 -1
  91. package/dist/features/studio/routes.js +3 -1
  92. package/dist/features/studio/showInCanvas.js +129 -0
  93. package/dist/features/vite/blockBoilerplate.js +3 -2
  94. package/dist/features/vite/definitionsSync.js +4 -5
  95. package/dist/features/vite/layoutGeneration.js +3 -2
  96. package/dist/features/vite/runtimeDev.js +32 -17
  97. package/dist/features/vite/staticAssetHeaders.js +52 -0
  98. package/dist/features/vite/vite.d.ts +1 -1
  99. package/dist/features/vite/vite.js +17 -4
  100. package/dist/lib/api-client-server.d.ts +1 -1
  101. package/dist/lib/api-client.d.ts +2 -1
  102. package/dist/lib/draft-scope.d.ts +1 -0
  103. package/dist/lib/locale-path.d.ts +3 -11
  104. package/dist/lib/locale-path.js +3 -27
  105. package/dist/lib/queries.d.ts +4 -0
  106. package/dist/lib/queries.js +8 -1
  107. package/dist/lib/shortcuts.js +29 -0
  108. package/dist/lib/studio-reads.d.ts +1 -0
  109. package/dist/lib/studio-reads.js +3 -1
  110. package/dist/lib/utils.js +1 -104
  111. package/dist/package.js +1 -1
  112. package/dist/studio.css +1 -1
  113. package/package.json +6 -4
  114. package/skills/camox/references/block-definitions.md +105 -25
  115. package/skills/camox/references/layout-definitions.md +53 -17
  116. package/skills/camox/references/routed-layouts.md +17 -14
  117. package/dist/features/studio/components/FaviconDialog.js +0 -318
@@ -5,24 +5,37 @@ 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
- 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";
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, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, declarePage, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readPageMarkdown, 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";
23
29
 
24
30
  //#region src/features/runtime/runtime.ts
25
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
+ ];
26
39
  function normalizeRuntimeBasePath(basePath) {
27
40
  if (basePath === void 0) return DEFAULT_RUNTIME_BASE_PATH;
28
41
  if (!basePath || basePath === "/") return "";
@@ -51,6 +64,26 @@ function getRequestDraftId(request, options) {
51
64
  function isPrivateRender(request, options) {
52
65
  return !!getServerAuthCookieHeader(request.headers) || getRequestDraftId(request, options) !== void 0;
53
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
+ }
54
87
  function escapeXml(value) {
55
88
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
56
89
  }
@@ -84,6 +117,10 @@ function matchRuntimeRoute(pathname) {
84
117
  kind: "studio-content",
85
118
  pathname
86
119
  };
120
+ if (pathname === "/camox/settings" || pathname === `${"/camox/settings"}/`) return {
121
+ kind: "studio-settings",
122
+ pathname
123
+ };
87
124
  if (pathname.startsWith("/camox/")) return {
88
125
  kind: "studio-nested",
89
126
  pathname
@@ -151,15 +188,22 @@ function absoluteUrl(request, options) {
151
188
  const { origin } = new URL(request.url);
152
189
  return (path) => `${origin}${withRuntimeBasePath(path, options.runtimeBasePath)}`;
153
190
  }
191
+ /** An ETag derived from `text`. */
192
+ async function etagOf(text) {
193
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
194
+ return `"${[...new Uint8Array(digest)].slice(0, 16).map((byte) => byte.toString(16).padStart(2, "0")).join("")}"`;
195
+ }
196
+ /** Whether the request's `If-None-Match` names `etag`, so a 304 answers it. */
197
+ function namesEtag(request, etag) {
198
+ return (request.headers.get("If-None-Match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === etag || tag === "*");
199
+ }
154
200
  /** `body` with an ETag derived from it, or a 304 when the request's `If-None-Match` names it. */
155
201
  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
202
  const taggedHeaders = {
159
203
  ...headers,
160
- ETag: etag
204
+ ETag: await etagOf(body)
161
205
  };
162
- if ((request.headers.get("If-None-Match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === etag || tag === "*")) return new Response(null, {
206
+ if (namesEtag(request, taggedHeaders.ETag)) return new Response(null, {
163
207
  status: 304,
164
208
  headers: taggedHeaders
165
209
  });
@@ -171,10 +215,7 @@ function createLlmsTxtResponse(request, options) {
171
215
  pages: website.localizations[0].pages,
172
216
  layouts: (website.app?.getLayouts() ?? []).map(({ _internal }) => _internal),
173
217
  url: absoluteUrl(request, options)
174
- }), {
175
- "Content-Type": "text/plain; charset=utf-8",
176
- "Cache-Control": "public, max-age=300, stale-while-revalidate=3600"
177
- }));
218
+ }), { "Content-Type": "text/plain; charset=utf-8" }));
178
219
  }
179
220
  /** Tells LLM tools fetching a page's HTML where the website's llms.txt is. */
180
221
  function llmsTxtLinkHeader(options) {
@@ -250,21 +291,94 @@ async function createRoutesResponse(request, options) {
250
291
  });
251
292
  }
252
293
  }
294
+ /**
295
+ * The image of the page `/og?path=` names, by its path with its locale's prefix: its layout's
296
+ * `toImage`, or Camox's default image, rendered with the page's context. Only real pages have
297
+ * one, read as the page itself would be, in the draft the URL names for a member.
298
+ */
253
299
  async function createOgResponse(request, options) {
254
300
  const camoxApp = await options.getCamoxApp?.();
255
301
  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
302
+ const source = await readPageImageSource(request, options, camoxApp);
303
+ if (!source) return new Response("Not found", { status: 404 });
304
+ const headers = {
305
+ ETag: await etagOf(JSON.stringify([source.layout?.id ?? null, source.context])),
306
+ "Cache-Control": isPrivateRender(request, options) ? "private, no-store" : "public, no-cache"
307
+ };
308
+ if (namesEtag(request, headers.ETag)) return new Response(null, {
309
+ status: 304,
310
+ headers
267
311
  });
312
+ const toImage = source.layout?.toImage ?? DefaultPageImage;
313
+ const image = await renderPageImage(toImage(source.context));
314
+ for (const [name, value] of Object.entries(headers)) image.headers.set(name, value);
315
+ return image;
316
+ }
317
+ /** The layout and context of the page `/og` names, or null when it names no page. */
318
+ async function readPageImageSource(request, options, app) {
319
+ const { apiUrl, projectUuid } = options;
320
+ const path = new URL(request.url).searchParams.get("path");
321
+ if (!apiUrl || !projectUuid || !path?.startsWith("/")) return null;
322
+ const located = await locateRequest(request, options, path);
323
+ if (matchLocalizedRoute(located.path)?.kind !== "page") return null;
324
+ const { project } = await resolveRequestMember(request, options);
325
+ const member = project !== void 0;
326
+ const authCookieHeader = member ? getServerAuthCookieHeader(request.headers) : void 0;
327
+ const requestedDraft = getRequestDraftId(request, options);
328
+ const api = (draftId) => createServerApiClient(apiUrl, {
329
+ authCookieHeader,
330
+ draftId,
331
+ locale: located.locale
332
+ });
333
+ const match = matchDerivedLayout(app.getLayouts(), located.path);
334
+ /** A code-owned page: its loader runs in the draft the layout read settled on. */
335
+ const codeOwned = async (match) => {
336
+ const layout = match.layout._internal;
337
+ const { value: shared, draftId } = await readDraftOrLive(requestedDraft, member, (draftId) => api(draftId).layouts.get({
338
+ projectUuid,
339
+ layoutId: layout.id
340
+ }));
341
+ const result = await loadLayoutResult(app, match, createLayoutRequestContext(options, member ? "draft" : "live", {
342
+ authCookieHeader,
343
+ draftId,
344
+ locale: located.locale
345
+ }), located.locale);
346
+ const record = asCollectionItem(result);
347
+ const collection = record && app.getCollectionById(record.collectionId);
348
+ return {
349
+ layout,
350
+ context: buildLayoutContext(layout, {
351
+ path,
352
+ locale: located.locale.tag,
353
+ projectName: shared.projectName,
354
+ data: result?.data,
355
+ params: match.params,
356
+ record: record && collection ? recordPageValues(collection, record) : void 0
357
+ })
358
+ };
359
+ };
360
+ /** A curated page, or else a derived layout's page, as the page's HTML falls back. */
361
+ const curated = async () => {
362
+ try {
363
+ const { value: page } = await readDraftOrLive(requestedDraft, member, (draftId) => api(draftId).pages.getByPath({
364
+ path: located.path,
365
+ projectUuid
366
+ }));
367
+ return {
368
+ layout: page.layout ? app.getLayoutById(page.layout.layoutId)?._internal : void 0,
369
+ context: curatedLayoutContext(page, located.locale)
370
+ };
371
+ } catch (error) {
372
+ if (isNotFoundError(error) && match) return codeOwned(match);
373
+ throw error;
374
+ }
375
+ };
376
+ try {
377
+ return match?.layout._internal.kind === "singleton" ? await codeOwned(match) : await curated();
378
+ } catch (error) {
379
+ if (isNotFoundError(error)) return null;
380
+ throw error;
381
+ }
268
382
  }
269
383
  function createDefaultHeadInput(stylesheetUrl) {
270
384
  return {
@@ -303,9 +417,9 @@ function createPageHeadInput(head) {
303
417
  const pageLinks = [];
304
418
  for (const entry of links) {
305
419
  if (!entry.href) continue;
306
- if (entry.rel === "icon") {
420
+ if (entry.rel === "icon" || entry.rel === "canonical") {
307
421
  pageLinks.push({
308
- rel: "icon",
422
+ rel: entry.rel,
309
423
  href: entry.href,
310
424
  "data-camox-page-head": "true"
311
425
  });
@@ -319,10 +433,16 @@ function createPageHeadInput(head) {
319
433
  "data-camox-page-head": "true"
320
434
  });
321
435
  }
436
+ const pageScripts = (head.scripts ?? []).filter((entry) => entry.type === "application/ld+json").map((entry) => ({
437
+ type: "application/ld+json",
438
+ innerHTML: entry.text,
439
+ "data-camox-page-head": "true"
440
+ }));
322
441
  return {
323
442
  ...title && { title },
324
443
  meta: pageMeta,
325
- link: pageLinks
444
+ link: pageLinks,
445
+ script: pageScripts
326
446
  };
327
447
  }
328
448
  function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl, locale) {
@@ -400,16 +520,114 @@ function isPreviewHandoff(request, options) {
400
520
  * The alternates of the page a request renders, where it exists in each of the scope's locales,
401
521
  * and its head with their `hreflang` links. Without a head, there is none to add them to.
402
522
  */
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));
523
+ async function readAlternates(request, options, { located, identity, head, described, localeReads }) {
524
+ const [alternates, crumbs] = await Promise.all([findRequestAlternates(options, located, identity, localeReads), head ? readRequestBreadcrumb(options, located, described.title, localeReads) : []]);
525
+ const url = absoluteUrl(request, options);
526
+ const links = hreflangLinks(alternates, located.locales, url);
527
+ const place = {
528
+ url: url(localizedPath(located.path, located.locale)),
529
+ locale: located.locale.tag,
530
+ alternates
531
+ };
532
+ const site = {
533
+ url: url("/"),
534
+ name: described.project.name,
535
+ logo: projectLogo(url, described.project),
536
+ socialProfiles: described.project.socialProfiles ?? []
537
+ };
538
+ const breadcrumb = crumbs.map((crumb) => ({
539
+ name: crumb.name,
540
+ url: url(localizedPath(crumb.path, located.locale))
541
+ }));
542
+ const defaultLocale = located.locales.find((locale) => locale.isDefault) ?? located.locale;
543
+ const pageUrl = (marker) => {
544
+ const locale = located.locales.find((candidate) => candidate.tag === marker.locale);
545
+ return url(localizedPath(marker.path, locale ?? defaultLocale));
546
+ };
547
+ const blocks = withPageUrls(described.blocks ?? [], pageUrl);
548
+ const localeHome = url(localizedPath("/", located.locale));
549
+ const records = described.records && {
550
+ ...described.records,
551
+ nodes: withPageUrls(described.records.nodes, pageUrl),
552
+ id: (recordId) => `${localeHome}#record-${recordId}`
553
+ };
406
554
  return {
407
555
  alternates,
408
- head: head && withHreflangs(head, links)
556
+ head: head && withStructuredData(withLocatedTags(withHreflangs(head, links), place), place, site, {
557
+ ...described,
558
+ breadcrumb,
559
+ blocks,
560
+ records
561
+ })
562
+ };
563
+ }
564
+ /** An Open Graph locale: a locale tag with an underscore, as in `de_CH`. */
565
+ const ogLocale = (tag) => tag.replaceAll("-", "_");
566
+ /**
567
+ * A page's head with where it is: its canonical URL, which never names a draft, and its locale
568
+ * among the locales it exists in.
569
+ */
570
+ function withLocatedTags(head, { url, locale, alternates }) {
571
+ const others = alternates.filter((alternate) => alternate.locale !== locale);
572
+ return {
573
+ ...head,
574
+ meta: [
575
+ ...head.meta ?? [],
576
+ {
577
+ property: "og:url",
578
+ content: url
579
+ },
580
+ {
581
+ property: "og:locale",
582
+ content: ogLocale(locale)
583
+ },
584
+ ...others.map((other) => ({
585
+ property: "og:locale:alternate",
586
+ content: ogLocale(other.locale)
587
+ }))
588
+ ],
589
+ links: [...head.links ?? [], {
590
+ rel: "canonical",
591
+ href: url
592
+ }]
593
+ };
594
+ }
595
+ function requestLocaleReads(options, { authCookieHeader, draftId, queryClient }) {
596
+ const { apiUrl, projectUuid } = options;
597
+ const reading = {
598
+ authCookieHeader: draftId ? authCookieHeader : void 0,
599
+ draftId
600
+ };
601
+ const source = draftId ? "draft" : "live";
602
+ const pagesByLocale = /* @__PURE__ */ new Map();
603
+ const studioCache = apiUrl && queryClient ? createServerStudioCache(queryClient, { apiUrl }) : void 0;
604
+ return {
605
+ pages: (locale) => {
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);
612
+ const read = pagesByLocale.get(locale.tag) ?? createServerApiClient(apiUrl, {
613
+ ...reading,
614
+ locale
615
+ }).pages.listBySlug({ projectUuid }).then(({ pages }) => pages);
616
+ pagesByLocale.set(locale.tag, read);
617
+ return read;
618
+ },
619
+ context: (locale) => createLayoutRequestContext(options, source, {
620
+ ...reading,
621
+ locale
622
+ }),
623
+ records: (locale) => createRecordLookup(options, source, {
624
+ ...reading,
625
+ locale
626
+ })
409
627
  };
410
628
  }
411
629
  /** 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 }) {
630
+ async function findRequestAlternates(options, located, identity, reads) {
413
631
  const { apiUrl, projectUuid } = options;
414
632
  const here = [{
415
633
  locale: located.locale.tag,
@@ -417,26 +635,33 @@ async function findRequestAlternates(options, located, identity, { authCookieHea
417
635
  }];
418
636
  if (!apiUrl || !projectUuid || located.locales.length < 2) return here;
419
637
  const app = await options.getCamoxApp?.();
420
- const reading = {
421
- authCookieHeader: draftId ? authCookieHeader : void 0,
422
- draftId
423
- };
424
638
  return findAlternates({
425
639
  identity,
426
640
  located,
427
641
  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
- }
642
+ reads
438
643
  });
439
644
  }
645
+ /**
646
+ * The crumbs of the page a request renders, in its locale. Without them, the page still renders:
647
+ * a breadcrumb that can't be read is left out.
648
+ */
649
+ async function readRequestBreadcrumb(options, located, title, reads) {
650
+ try {
651
+ return await readBreadcrumb({
652
+ path: located.path,
653
+ title,
654
+ app: await options.getCamoxApp?.(),
655
+ reads: {
656
+ pages: () => reads.pages(located.locale),
657
+ records: reads.records(located.locale)
658
+ }
659
+ });
660
+ } catch (error) {
661
+ console.error("[camox] Couldn't read the page's breadcrumb.", error);
662
+ return [];
663
+ }
664
+ }
440
665
  /** A page's head with its alternates' `hreflang` links. */
441
666
  function withHreflangs(head, hreflangs) {
442
667
  if (!hreflangs.length) return head;
@@ -478,27 +703,89 @@ async function createPreviewHandoffResponse({ options, pathname, request }) {
478
703
  "Referrer-Policy": "no-referrer"
479
704
  } });
480
705
  }
481
- 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 }) {
482
772
  if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !options.renderPage) return createPageDataResponse({
483
773
  options,
484
774
  pathname,
485
775
  located,
486
- request
776
+ request,
777
+ pageRead
487
778
  });
488
779
  const singleton = await createDerivedResponse(options, pathname, located, request, { singletonOnly: true });
489
780
  if (singleton) return singleton;
490
- 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);
491
787
  try {
492
- const result = await loadCamoxPageForRequest({
493
- apiUrl: options.apiUrl,
494
- authCookieHeader: getServerAuthCookieHeader(request.headers),
495
- draftId: getRequestDraftId(request, options),
496
- locale: located.locale,
497
- origin: new URL(request.url).origin,
498
- pathname: located.path,
499
- projectUuid: options.projectUuid,
500
- queryClient
501
- });
788
+ const result = await reading;
502
789
  const headers = new Headers({
503
790
  "Content-Type": "text/html; charset=utf-8",
504
791
  Vary: "Cookie",
@@ -515,8 +802,8 @@ async function createPageHtmlResponse({ options, pathname, located, request }) {
515
802
  pageId: result.data.page.page.id
516
803
  },
517
804
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : {},
518
- authCookieHeader: getServerAuthCookieHeader(request.headers),
519
- draftId: result.draftId
805
+ described: curatedPageDescription(camoxApp, result.data.page, located.locale),
806
+ localeReads: localeReads(result.draftId)
520
807
  });
521
808
  const dehydratedState = dehydrate(queryClient);
522
809
  const { project } = result;
@@ -662,7 +949,7 @@ async function createLayoutMarkdownResponse(options, match, locale, page) {
662
949
  toMarkdown: template
663
950
  })],
664
951
  data,
665
- item: recordPlainText(collection, data.content)
952
+ record: recordPageValues(collection, record)
666
953
  };
667
954
  };
668
955
  let rendered;
@@ -685,13 +972,14 @@ async function createLayoutMarkdownResponse(options, match, locale, page) {
685
972
  });
686
973
  const meta = buildPageMeta({
687
974
  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
975
+ context: buildLayoutContext(match.layout._internal, {
976
+ path: page.pathname,
977
+ locale: locale.tag,
978
+ projectName: slots.projectName,
979
+ data: rendered.data,
980
+ params: match.params,
981
+ record: "record" in rendered ? rendered.record : void 0
982
+ })
695
983
  });
696
984
  const markdown = [
697
985
  slots.before,
@@ -746,6 +1034,15 @@ async function createDerivedResponse(options, pathname, located, request, { data
746
1034
  }), serializedResult ?? null, shownIn);
747
1035
  const record = asCollectionItem(serializedResult);
748
1036
  const recordCollection = record && app.getCollectionById(record.collectionId);
1037
+ const recordValues = record && recordCollection ? recordPageValues(recordCollection, record) : void 0;
1038
+ const context = buildLayoutContext(match.layout._internal, {
1039
+ path: pathname,
1040
+ locale: located.locale.tag,
1041
+ projectName: shared.projectName,
1042
+ data: serializedResult?.data,
1043
+ params: match.params,
1044
+ record: recordValues
1045
+ });
749
1046
  const { alternates, head: pageHead } = await readAlternates(request, options, {
750
1047
  located,
751
1048
  identity: record && match.layout._internal.kind === "derived" ? {
@@ -755,19 +1052,34 @@ async function createDerivedResponse(options, pathname, located, request, { data
755
1052
  } : { kind: "code-owned" },
756
1053
  head: buildPageHead({
757
1054
  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,
1055
+ context,
765
1056
  origin: new URL(request.url).origin,
766
1057
  faviconUrl: buildFaviconUrl(options.apiUrl, shared.project),
767
- hasMarkdown: match.layout._internal.toMarkdown !== void 0
1058
+ hasMarkdown: match.layout._internal.toMarkdown !== void 0,
1059
+ recordType: record?.structuredData?.node?.["@type"]
768
1060
  }),
769
- authCookieHeader,
770
- draftId: read.draftId
1061
+ described: {
1062
+ project: {
1063
+ ...shared.project,
1064
+ name: shared.projectName
1065
+ },
1066
+ title: context.page.title,
1067
+ declared: declarePage({
1068
+ layout: match.layout._internal,
1069
+ context
1070
+ }),
1071
+ blocks: placedBlockNodes(shared.layout, [], shared.structuredData),
1072
+ ...record?.published && { published: record.published },
1073
+ records: pageRecords(shared.recordStructuredData, record && {
1074
+ id: record.data.id,
1075
+ structuredData: record.structuredData,
1076
+ titles: recordValues?.titles
1077
+ })
1078
+ },
1079
+ localeReads: requestLocaleReads(options, {
1080
+ authCookieHeader,
1081
+ draftId: read.draftId
1082
+ })
771
1083
  });
772
1084
  const analytics = dataOnly ? void 0 : visitorAnalytics(options, {
773
1085
  studio,
@@ -794,6 +1106,11 @@ async function createDerivedResponse(options, pathname, located, request, { data
794
1106
  locales: located.locales,
795
1107
  derived: {
796
1108
  layoutId: match.layout._internal.id,
1109
+ page: {
1110
+ params: match.params,
1111
+ path: pathname,
1112
+ projectName: shared.projectName
1113
+ },
797
1114
  result: serializedResult,
798
1115
  layout: shared.layout
799
1116
  }
@@ -801,7 +1118,7 @@ async function createDerivedResponse(options, pathname, located, request, { data
801
1118
  const document = await options.getDocument?.() ?? {};
802
1119
  await preparePreviewDocument(input, options, document, !studio && !dataOnly ? "" : void 0);
803
1120
  if (dataOnly) return Response.json(input, { headers: {
804
- "Cache-Control": "private, no-store",
1121
+ ...isPrivateRender(request, options) ? { "Cache-Control": "private, no-store" } : {},
805
1122
  Vary: "Cookie",
806
1123
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
807
1124
  } });
@@ -915,30 +1232,26 @@ async function createCanvasOnlyResponse({ options, pathname, located, request, d
915
1232
  ...headers
916
1233
  } });
917
1234
  }
918
- async function createPageDataResponse({ options, pathname, located, request }) {
1235
+ async function createPageDataResponse({ options, pathname, located, request, pageRead }) {
919
1236
  if (!options.apiUrl || !options.projectUuid) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
920
1237
  const singleton = await createDerivedResponse(options, pathname, located, request, {
921
1238
  dataOnly: true,
922
1239
  singletonOnly: true
923
1240
  });
924
1241
  if (singleton) return singleton;
925
- 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);
926
1248
  try {
927
- const result = await loadCamoxPageForRequest({
928
- apiUrl: options.apiUrl,
929
- authCookieHeader: getServerAuthCookieHeader(request.headers),
930
- draftId: getRequestDraftId(request, options),
931
- locale: located.locale,
932
- origin: new URL(request.url).origin,
933
- pathname: located.path,
934
- projectUuid: options.projectUuid,
935
- queryClient
936
- });
1249
+ const result = await reading;
937
1250
  const headers = new Headers({
938
1251
  "Content-Type": "application/json; charset=utf-8",
939
- "Cache-Control": "private, no-store",
940
1252
  Vary: "Cookie"
941
1253
  });
1254
+ if (isPrivateRender(request, options)) headers.set("Cache-Control", "private, no-store");
942
1255
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
943
1256
  const camoxApp = await options.getCamoxApp?.();
944
1257
  const { alternates, head } = await readAlternates(request, options, {
@@ -948,8 +1261,8 @@ async function createPageDataResponse({ options, pathname, located, request }) {
948
1261
  pageId: result.data.page.page.id
949
1262
  },
950
1263
  head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null,
951
- authCookieHeader: getServerAuthCookieHeader(request.headers),
952
- draftId: result.draftId
1264
+ described: curatedPageDescription(camoxApp, result.data.page, located.locale),
1265
+ localeReads: localeReads(result.draftId)
953
1266
  });
954
1267
  const dehydratedState = dehydrate(queryClient);
955
1268
  const input = {
@@ -1000,6 +1313,7 @@ async function handleCamoxRequest(request, options = {}) {
1000
1313
  });
1001
1314
  }
1002
1315
  if (match.kind === "og") return createOgResponse(request, options);
1316
+ if (match.kind === "logo") return createProjectLogoResponse(request, options);
1003
1317
  if (match.kind === "sitemap") return createSitemapResponse(request, options);
1004
1318
  if (match.kind === "llms-txt") return createLlmsTxtResponse(request, options);
1005
1319
  if (match.kind === "routes") return createRoutesResponse(request, options);
@@ -1016,19 +1330,17 @@ async function handleCamoxRequest(request, options = {}) {
1016
1330
  const { apiUrl, projectUuid } = options;
1017
1331
  const markdownPathname = parseMarkdownPath(pathname);
1018
1332
  if (apiUrl && projectUuid && (markdownPathname || prefersMarkdown(request.headers.get("Accept") ?? ""))) {
1019
- const located = await locateRequest(request, options, markdownPathname ?? pathname, { live: true });
1020
- if (markdownPathname || matchLocalizedRoute(located.path)?.kind === "page") {
1021
- const markdownResponse = await createPageMarkdownResponse({
1022
- ...options,
1023
- apiUrl,
1024
- projectUuid
1025
- }, located, {
1026
- pathname: markdownPathname ?? pathname,
1027
- request
1028
- });
1029
- if (markdownResponse) return markdownResponse;
1030
- if (markdownPathname) return new Response("Page not found", { status: 404 });
1031
- }
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 });
1032
1344
  }
1033
1345
  return respondInLocale(options, request, pathname);
1034
1346
  }
@@ -1037,7 +1349,7 @@ async function handleCamoxRequest(request, options = {}) {
1037
1349
  * With `dataOnly`, as the JSON client navigation loads.
1038
1350
  */
1039
1351
  async function respondInLocale(options, request, pathname, { dataOnly = false } = {}) {
1040
- const located = await locateRequest(request, options, pathname);
1352
+ const [early, located] = await Promise.all([readDefaultLocalePageEarly(options, request, pathname), locateRequest(request, options, pathname)]);
1041
1353
  const localized = matchLocalizedRoute(located.path);
1042
1354
  if (!localized) return new Response("Not found", { status: 404 });
1043
1355
  const target = {
@@ -1055,13 +1367,29 @@ async function respondInLocale(options, request, pathname, { dataOnly = false }
1055
1367
  match: localized,
1056
1368
  dataOnly
1057
1369
  });
1058
- 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);
1059
1375
  }
1060
1376
  /**
1061
1377
  * A page's Markdown Representation in its locale, with the same precedence as its HTML:
1062
- * 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.
1063
1380
  */
1064
- 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;
1065
1393
  const page = {
1066
1394
  pathname,
1067
1395
  url: absoluteUrl(request, options)(pathname)
@@ -1069,20 +1397,12 @@ async function createPageMarkdownResponse(options, { locale, path }, { pathname,
1069
1397
  const app = await options.getCamoxApp?.();
1070
1398
  const layoutMatch = app && matchDerivedLayout(app.getLayouts(), path);
1071
1399
  if (layoutMatch?.layout._internal.kind === "singleton") return createLayoutMarkdownResponse(options, layoutMatch, locale, page);
1072
- return createMarkdownResponse({
1073
- apiUrl: options.apiUrl,
1074
- camoxApp: app,
1075
- locale,
1076
- pathname: path,
1077
- projectUuid: options.projectUuid,
1078
- request,
1079
- url: page.url,
1080
- onNotFound: async () => layoutMatch ? createLayoutMarkdownResponse(options, layoutMatch, locale, page) : null
1081
- });
1400
+ if (read.page) return curatedMarkdownResponse(app, read, page.url);
1401
+ return layoutMatch ? createLayoutMarkdownResponse(options, layoutMatch, locale, page) : null;
1082
1402
  }
1083
1403
  /** Whether a route is one of the studio's own, rather than a page or the Canvas. */
1084
1404
  function isStudioRoute(match) {
1085
- return match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested";
1405
+ return STUDIO_ROUTE_KINDS.includes(match.kind);
1086
1406
  }
1087
1407
  /**
1088
1408
  * The studio or page route a locale-relative path names, or null for a runtime-owned route
@@ -1094,19 +1414,16 @@ function matchLocalizedRoute(path) {
1094
1414
  }
1095
1415
  /**
1096
1416
  * The locale a request's path names, among the locales of the scope it reads: the URL's draft's
1097
- * for a session naming one, else Live's. `live` reads Live's whatever the request names. Without
1098
- * an API there are none to read: every path is the default locale's, which then says it can't
1099
- * 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.
1100
1419
  */
1101
- async function locateRequest(request, options, pathname, { live = false } = {}) {
1420
+ async function locateRequest(request, options, pathname) {
1102
1421
  const { apiUrl, projectUuid } = options;
1103
1422
  const locales = apiUrl && projectUuid ? await readScopeLocales({
1104
1423
  apiUrl,
1105
1424
  projectUuid,
1106
- ...!live && {
1107
- authCookieHeader: getServerAuthCookieHeader(request.headers),
1108
- draftId: getRequestDraftId(request, options)
1109
- }
1425
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
1426
+ draftId: getRequestDraftId(request, options)
1110
1427
  }) : [UNNAMED_DEFAULT_LOCALE];
1111
1428
  return {
1112
1429
  ...parseLocalePath(pathname, locales),
@@ -1115,4 +1432,4 @@ async function locateRequest(request, options, pathname, { live = false } = {})
1115
1432
  }
1116
1433
 
1117
1434
  //#endregion
1118
- export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, matchRuntimeRoute };
1435
+ export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, isPublicRequest, matchRuntimeRoute };