camox 0.47.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/createApp.d.ts +19 -6
- package/dist/core/createApp.js +7 -2
- package/dist/core/createBlock.js +54 -43
- package/dist/core/createCollection.d.ts +25 -7
- package/dist/core/createCollection.js +28 -9
- package/dist/core/createLayout.d.ts +98 -71
- package/dist/core/createLayout.js +10 -16
- package/dist/core/derivedRoutes.js +1 -1
- package/dist/core/editing/BlockEditingRuntime.d.ts +3 -2
- package/dist/core/editing/CompleteBlockEditingRuntime.js +2 -1
- package/dist/core/editing/createEditableBlock.d.ts +59 -19
- package/dist/core/editing/createEditableBlock.js +162 -151
- package/dist/core/editing/useLatestRecord.js +7 -7
- package/dist/core/layoutContext.d.ts +55 -0
- package/dist/core/layoutContext.js +66 -0
- package/dist/core/lib/contentType.d.ts +91 -4
- package/dist/core/lib/contentType.js +84 -7
- package/dist/core/lib/recordScope.js +19 -17
- package/dist/core/lib/reference.d.ts +6 -2
- package/dist/core/recordItem.js +55 -2
- package/dist/core/requestContext.d.ts +18 -0
- package/dist/core/structuredData.d.ts +126 -0
- package/dist/core/structuredData.js +40 -0
- package/dist/features/canvas/canvasCamera.js +1 -1
- package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
- package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
- package/dist/features/content/ContentAssets.js +84 -88
- package/dist/features/content/ContentCollection.js +13 -7
- package/dist/features/content/collection-form.js +6 -2
- package/dist/features/content/components/MissingFromLocale.js +1 -1
- package/dist/features/og/pageImage.js +89 -0
- package/dist/features/page/CuratedPageContent.js +45 -32
- package/dist/features/page/DerivedPageContent.js +49 -22
- package/dist/features/page/SharedLayoutContent.js +26 -22
- package/dist/features/preview/CamoxPreview.js +41 -27
- package/dist/features/preview/EditablePageExperience.js +17 -9
- package/dist/features/preview/components/AddBlockSheet.js +1 -0
- package/dist/features/preview/components/BlockActionsPopover.js +19 -41
- package/dist/features/preview/components/FieldToolbar.js +1 -1
- package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
- package/dist/features/preview/components/PageInfo.js +244 -264
- package/dist/features/preview/components/PageTree.js +63 -53
- package/dist/features/preview/components/PreviewPanel.js +10 -64
- package/dist/features/preview/components/PreviewToolbar.js +1 -1
- package/dist/features/preview/components/RecordCard.js +50 -34
- package/dist/features/preview/components/RecordCombobox.js +12 -5
- package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
- package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
- package/dist/features/preview/components/RepeatableItemsList.js +62 -46
- package/dist/features/preview/components/SelectionEditor.js +3 -2
- package/dist/features/preview/components/useFieldWriter.js +2 -4
- package/dist/features/preview/components/useRecordView.js +2 -2
- package/dist/features/preview/entityName.js +11 -0
- package/dist/features/preview/pageRecord.js +17 -18
- package/dist/features/preview/previewConstants.js +2 -8
- package/dist/features/preview/usePreviewViewportActions.js +1 -1
- package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
- package/dist/features/provider/components/CommandPalette.js +2 -5
- package/dist/features/provider/signInShortcut.js +1 -2
- package/dist/features/provider/useAdminShortcuts.js +47 -53
- package/dist/features/routes/pageRuntime.d.ts +17 -2
- package/dist/features/routes/pageRuntime.js +61 -57
- package/dist/features/runtime/RuntimeChrome.js +1 -1
- package/dist/features/runtime/breadcrumbs.js +68 -0
- package/dist/features/runtime/livePages.js +3 -2
- package/dist/features/runtime/pageGraph.js +92 -0
- package/dist/features/runtime/projectLogo.js +110 -0
- package/dist/features/runtime/publicCache.js +83 -0
- package/dist/features/runtime/runtime.d.ts +24 -9
- package/dist/features/runtime/runtime.js +447 -130
- package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
- package/dist/features/runtime/runtimeRoutes.js +1 -0
- package/dist/features/runtime/structuredData.js +213 -0
- package/dist/features/runtime/worker.d.ts +5 -1
- package/dist/features/runtime/worker.js +20 -3
- package/dist/features/settings/AnalyticsSettings.js +622 -0
- package/dist/features/settings/CamoxSettings.js +95 -0
- package/dist/features/settings/FaviconSettings.js +254 -0
- package/dist/features/settings/ProjectNameSettings.js +145 -0
- package/dist/features/settings/SettingsPart.js +73 -0
- package/dist/features/settings/SocialProfilesSettings.js +245 -0
- package/dist/features/settings/useProjectMutation.js +61 -0
- package/dist/features/studio/components/ChatSidebar.js +5 -5
- package/dist/features/studio/components/DraftsControl.js +136 -131
- package/dist/features/studio/components/LocalePicker.js +178 -174
- package/dist/features/studio/components/Navbar.js +107 -66
- package/dist/features/studio/components/ProjectMenu.js +85 -110
- package/dist/features/studio/components/ReviewDraftSheet.js +1046 -990
- package/dist/features/studio/components/ThreadMessages.js +2 -2
- package/dist/features/studio/components/UserButton.js +1 -1
- package/dist/features/studio/routes.js +3 -1
- package/dist/features/studio/showInCanvas.js +129 -0
- package/dist/features/vite/blockBoilerplate.js +3 -2
- package/dist/features/vite/definitionsSync.js +4 -5
- package/dist/features/vite/layoutGeneration.js +3 -2
- package/dist/features/vite/runtimeDev.js +32 -17
- package/dist/features/vite/staticAssetHeaders.js +52 -0
- package/dist/features/vite/vite.d.ts +1 -1
- package/dist/features/vite/vite.js +17 -4
- package/dist/lib/api-client-server.d.ts +1 -1
- package/dist/lib/api-client.d.ts +2 -1
- package/dist/lib/draft-scope.d.ts +1 -0
- package/dist/lib/locale-path.d.ts +3 -11
- package/dist/lib/locale-path.js +3 -27
- package/dist/lib/queries.d.ts +4 -0
- package/dist/lib/queries.js +8 -1
- package/dist/lib/shortcuts.js +29 -0
- package/dist/lib/studio-reads.d.ts +1 -0
- package/dist/lib/studio-reads.js +3 -1
- package/dist/lib/utils.js +1 -104
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +6 -4
- package/skills/camox/references/block-definitions.md +105 -25
- package/skills/camox/references/layout-definitions.md +53 -17
- package/skills/camox/references/routed-layouts.md +17 -14
- package/dist/features/studio/components/FaviconDialog.js +0 -318
|
@@ -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,
|
|
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,
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
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:
|
|
204
|
+
ETag: await etagOf(body)
|
|
161
205
|
};
|
|
162
|
-
if ((request
|
|
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
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
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:
|
|
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,
|
|
404
|
-
const alternates = await findRequestAlternates(options, located, identity,
|
|
405
|
-
const
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
770
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
952
|
-
|
|
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
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
1073
|
-
|
|
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
|
|
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.
|
|
1098
|
-
*
|
|
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
|
|
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
|
-
|
|
1107
|
-
|
|
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 };
|